From 3ef628ff7dfc38b8a4a8075fd9d7c5677f48829a Mon Sep 17 00:00:00 2001 From: Laurents Meyer Date: Tue, 27 Feb 2024 23:36:39 +0100 Subject: [PATCH] Update to EF Core 8.0.1 (#1852) * Update dependencies. * Update tests for EF Core 8.0.1. * Fix NTS source type mappings. * Implement MySqlRuntimeModelConvention and MySqlCSharpRuntimeAnnotationCodeGenerator. Update model extension methods. * Fix identity columns. * Improve test utilities. * Refactor MySqlParameterInliningExpressionVisitor. * Adjust TransactionMySqlTest. * Ensure different default database names for Northwind databases with different collations. * Correct MySqlBug96947Workaround version range. --- Dependencies.targets | 10 +- dotnet-tools.json | 2 +- .../Internal/MySqlGeometryMethodTranslator.cs | 4 +- ...NetTopologySuiteTypeMappingSourcePlugin.cs | 136 +++++++------- .../Internal/MySqlAnnotationCodeGenerator.cs | 107 +++++++++-- ...SqlCSharpRuntimeAnnotationCodeGenerator.cs | 169 ++++++++++++++++++ .../Extensions/MySqlEntityTypeExtensions.cs | 41 +++-- .../Extensions/MySqlIndexExtensions.cs | 18 +- .../Extensions/MySqlKeyExtensions.cs | 6 +- .../Extensions/MySqlModelBuilderExtensions.cs | 64 ++++++- .../Extensions/MySqlModelExtensions.cs | 53 +++--- .../MySqlPropertyBuilderExtensions.cs | 78 +++++++- .../Extensions/MySqlPropertyExtensions.cs | 96 ++++++---- .../Infrastructure/MySqlServerVersion.cs | 2 +- .../Conventions/MySqlConventionSetBuilder.cs | 6 + .../MySqlRuntimeModelConvention.cs | 133 ++++++++++++++ .../MySqlValueGenerationStrategyConvention.cs | 109 +++++++++++ .../Migrations/MySqlMigrationsSqlGenerator.cs | 2 +- ...MySqlParameterInliningExpressionVisitor.cs | 78 +++++--- .../BadDataJsonDeserializationMySqlTest.cs | 10 ++ .../PrimitiveCollectionsQueryMySqlTest.cs | 30 ++++ .../Query/TPCGearsOfWarQueryMySqlTest.cs | 82 +++++++++ .../MySqlNorthwindTestStoreFactory.cs | 8 +- .../TestUtilities/MySqlTestStoreFactory.cs | 6 +- .../TransactionMySqlTest.cs | 3 +- 25 files changed, 1047 insertions(+), 206 deletions(-) create mode 100644 src/EFCore.MySql/Metadata/Conventions/MySqlRuntimeModelConvention.cs create mode 100644 src/EFCore.MySql/Metadata/Conventions/MySqlValueGenerationStrategyConvention.cs create mode 100644 test/EFCore.MySql.FunctionalTests/BadDataJsonDeserializationMySqlTest.cs diff --git a/Dependencies.targets b/Dependencies.targets index db054e903..33f6c76da 100644 --- a/Dependencies.targets +++ b/Dependencies.targets @@ -1,6 +1,6 @@ - [8.0.0,8.0.999] + [8.0.1,8.0.999] @@ -13,16 +13,16 @@ - + - - + + - + diff --git a/dotnet-tools.json b/dotnet-tools.json index e3cadb92f..fd9a39d87 100644 --- a/dotnet-tools.json +++ b/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "8.0.0", + "version": "8.0.1", "commands": [ "dotnet-ef" ] diff --git a/src/EFCore.MySql.NTS/Query/Internal/MySqlGeometryMethodTranslator.cs b/src/EFCore.MySql.NTS/Query/Internal/MySqlGeometryMethodTranslator.cs index b2aa00140..7e8724b35 100644 --- a/src/EFCore.MySql.NTS/Query/Internal/MySqlGeometryMethodTranslator.cs +++ b/src/EFCore.MySql.NTS/Query/Internal/MySqlGeometryMethodTranslator.cs @@ -127,7 +127,7 @@ public virtual SqlExpression Translate(SqlExpression instance, MethodInfo method _sqlExpressionFactory.Constant(1)) }, method.ReturnType, - _typeMappingSource.FindMapping(method.ReturnType, storeType), + _typeMappingSource.FindMapping(method.ReturnType), false); } @@ -137,7 +137,7 @@ public virtual SqlExpression Translate(SqlExpression instance, MethodInfo method instance, arguments[0], method.ReturnType, - _typeMappingSource.FindMapping(method.ReturnType, storeType)); + _typeMappingSource.FindMapping(method.ReturnType)); } if (Equals(method, _isWithinDistance)) diff --git a/src/EFCore.MySql.NTS/Storage/Internal/MySqlNetTopologySuiteTypeMappingSourcePlugin.cs b/src/EFCore.MySql.NTS/Storage/Internal/MySqlNetTopologySuiteTypeMappingSourcePlugin.cs index 52d15d756..c9bbd5d21 100644 --- a/src/EFCore.MySql.NTS/Storage/Internal/MySqlNetTopologySuiteTypeMappingSourcePlugin.cs +++ b/src/EFCore.MySql.NTS/Storage/Internal/MySqlNetTopologySuiteTypeMappingSourcePlugin.cs @@ -48,19 +48,6 @@ public class MySqlNetTopologySuiteTypeMappingSourcePlugin : IRelationalTypeMappi { "multipolygon", typeof(MultiPolygon) }, // geometry -> geometrycollection -> multisurface -> multipolygon }; - private static readonly Dictionary _spatialClrTypeMappings = new Dictionary - { - { typeof(Geometry), "geometry" }, // geometry - { typeof(Point), "point" }, // geometry -> point - { typeof(LineString), "linestring" }, // geometry -> curve -> linestring - { typeof(LinearRing), "linearring" }, // geometry -> curve -> linestring -> linearring - { typeof(Polygon), "polygon" }, // geometry -> surface -> polygon - { typeof(GeometryCollection), "geometrycollection" }, // geometry -> geometrycollection - { typeof(MultiPoint), "multipoint" }, // geometry -> geometrycollection -> multipoint - { typeof(MultiLineString), "multilinestring" }, // geometry -> geometrycollection -> multicurve -> multilinestring - { typeof(MultiPolygon), "multipolygon" }, // geometry -> geometrycollection -> multisurface -> multipolygon - }; - private readonly NtsGeometryServices _geometryServices; private readonly IMySqlOptions _options; @@ -89,72 +76,75 @@ public MySqlNetTopologySuiteTypeMappingSourcePlugin( public virtual RelationalTypeMapping FindMapping(in RelationalTypeMappingInfo mappingInfo) { var clrType = mappingInfo.ClrType; - var storeTypeName = mappingInfo.StoreTypeName; - var storeTypeNameBase = mappingInfo.StoreTypeNameBase; + var storeTypeName = mappingInfo.StoreTypeName?.ToLowerInvariant(); + string defaultStoreType = null; + Type defaultClrType = null; + + return (clrType != null + && TryGetDefaultStoreType(clrType, out defaultStoreType)) + || (storeTypeName != null + && _spatialStoreTypeMappings.TryGetValue(storeTypeName, out defaultClrType)) + ? (RelationalTypeMapping)Activator.CreateInstance( + typeof(MySqlGeometryTypeMapping<>).MakeGenericType(clrType ?? defaultClrType ?? typeof(Geometry)), + _geometryServices, + storeTypeName ?? defaultStoreType ?? "geometry", + _options) + : null; + } - if (storeTypeName != null) + private static bool TryGetDefaultStoreType(Type type, out string defaultStoreType) + { + // geometry -> geometrycollection -> multisurface -> multipolygon + if (typeof(MultiPolygon).IsAssignableFrom(type)) { - // First look for the fully qualified store type name. - if (_spatialStoreTypeMappings.TryGetValue(storeTypeName, out var mappedClrType)) - { - // We found the user-specified store type. - // If no CLR type was provided, we're probably scaffolding from an existing database. Take the first - // mapping as the default. - // If a CLR type was provided, look for a mapping between the store and CLR types. If none is found, - // fail immediately. - clrType = clrType == null - ? mappedClrType - : clrType.IsAssignableFrom(mappedClrType) - ? clrType - : null; - - return clrType == null - ? null - : (RelationalTypeMapping)Activator.CreateInstance( - typeof(MySqlGeometryTypeMapping<>).MakeGenericType(clrType), - _geometryServices, - storeTypeName, - _options); - } - - // Then look for the base store type name. - if (_spatialStoreTypeMappings.TryGetValue(storeTypeNameBase, out mappedClrType)) - { - clrType = clrType == null - ? mappedClrType - : clrType.IsAssignableFrom(mappedClrType) - ? clrType - : null; - - if (clrType == null) - { - return null; - } - - var typeMapping = (RelationalTypeMapping)Activator.CreateInstance( - typeof(MySqlGeometryTypeMapping<>).MakeGenericType(clrType), - _geometryServices, - storeTypeName, - _options); - - return typeMapping.Clone(mappingInfo); - } - - // A store type name was provided, but is unknown. This could be a domain (alias) type, in which case - // we proceed with a CLR type lookup (if the type doesn't exist at all the failure will come later). + defaultStoreType = "multipolygon"; } - - if (clrType != null && - _spatialClrTypeMappings.TryGetValue(clrType, out var mappedStoreTypeName)) + // geometry -> geometrycollection -> multicurve -> multilinestring + else if (typeof(MultiLineString).IsAssignableFrom(type)) { - return (RelationalTypeMapping)Activator.CreateInstance( - typeof(MySqlGeometryTypeMapping<>).MakeGenericType(clrType), - _geometryServices, - mappedStoreTypeName, - _options); + defaultStoreType = "multilinestring"; + } + // geometry -> geometrycollection -> multipoint + else if (typeof(MultiPoint).IsAssignableFrom(type)) + { + defaultStoreType = "multipoint"; + } + // geometry -> geometrycollection + else if (typeof(GeometryCollection).IsAssignableFrom(type)) + { + defaultStoreType = "geometrycollection"; + } + // geometry -> surface -> polygon + else if (typeof(Polygon).IsAssignableFrom(type)) + { + defaultStoreType = "polygon"; + } + // geometry -> curve -> linestring -> linearring + else if (typeof(LinearRing).IsAssignableFrom(type)) + { + defaultStoreType = "linearring"; + } + // geometry -> curve -> linestring + else if (typeof(LineString).IsAssignableFrom(type)) + { + defaultStoreType = "linestring"; + } + // geometry -> point + else if (typeof(Point).IsAssignableFrom(type)) + { + defaultStoreType = "point"; + } + // geometry + else if (typeof(Geometry).IsAssignableFrom(type)) + { + defaultStoreType = "geometry"; + } + else + { + defaultStoreType = null; } - return null; + return defaultStoreType != null; } } } diff --git a/src/EFCore.MySql/Design/Internal/MySqlAnnotationCodeGenerator.cs b/src/EFCore.MySql/Design/Internal/MySqlAnnotationCodeGenerator.cs index 07d0d9cde..d0062b132 100644 --- a/src/EFCore.MySql/Design/Internal/MySqlAnnotationCodeGenerator.cs +++ b/src/EFCore.MySql/Design/Internal/MySqlAnnotationCodeGenerator.cs @@ -4,9 +4,9 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; -using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -19,6 +19,11 @@ namespace Pomelo.EntityFrameworkCore.MySql.Design.Internal { public class MySqlAnnotationCodeGenerator : AnnotationCodeGenerator { + private static readonly MethodInfo _modelUseIdentityColumnsMethodInfo + = typeof(MySqlModelBuilderExtensions).GetRequiredRuntimeMethod( + nameof(MySqlModelBuilderExtensions.AutoIncrementColumns), + typeof(ModelBuilder)); + private static readonly MethodInfo _modelHasCharSetMethodInfo = typeof(MySqlModelBuilderExtensions).GetRequiredRuntimeMethod( nameof(MySqlModelBuilderExtensions.HasCharSet), @@ -39,6 +44,12 @@ private static readonly MethodInfo _modelUseGuidCollationMethodInfo typeof(ModelBuilder), typeof(string)); + private static readonly MethodInfo _modelHasAnnotationMethodInfo + = typeof(ModelBuilder).GetRequiredRuntimeMethod( + nameof(ModelBuilder.HasAnnotation), + typeof(string), + typeof(object)); + private static readonly MethodInfo _entityTypeHasCharSetMethodInfo = typeof(MySqlEntityTypeBuilderExtensions).GetRequiredRuntimeMethod( nameof(MySqlEntityTypeBuilderExtensions.HasCharSet), @@ -53,13 +64,23 @@ private static readonly MethodInfo _entityTypeUseCollationMethodInfo typeof(string), typeof(DelegationModes?)); + private static readonly MethodInfo _propertyUseIdentityColumnMethodInfo + = typeof(MySqlPropertyBuilderExtensions).GetRequiredRuntimeMethod( + nameof(MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn), + typeof(PropertyBuilder)); + + private static readonly MethodInfo _propertyUseComputedColumnMethodInfo + = typeof(MySqlPropertyBuilderExtensions).GetRequiredRuntimeMethod( + nameof(MySqlPropertyBuilderExtensions.UseMySqlComputedColumn), + typeof(PropertyBuilder)); + private static readonly MethodInfo _propertyHasCharSetMethodInfo = typeof(MySqlPropertyBuilderExtensions).GetRequiredRuntimeMethod( nameof(MySqlPropertyBuilderExtensions.HasCharSet), typeof(PropertyBuilder), typeof(string)); - public MySqlAnnotationCodeGenerator([NotNull] AnnotationCodeGeneratorDependencies dependencies) + public MySqlAnnotationCodeGenerator([JetBrains.Annotations.NotNull] AnnotationCodeGeneratorDependencies dependencies) : base(dependencies) { } @@ -94,7 +115,7 @@ protected override MethodCallCodeFragment GenerateFluentApi(IModel model, IAnnot var delegationModes = model[MySqlAnnotationNames.CharSetDelegation] as DelegationModes?; return new MethodCallCodeFragment( _modelHasCharSetMethodInfo, - new[] {annotation.Value} + new[] { annotation.Value } .AppendIfTrue(delegationModes.HasValue, delegationModes) .ToArray()); } @@ -115,7 +136,7 @@ protected override MethodCallCodeFragment GenerateFluentApi(IModel model, IAnnot var delegationModes = model[MySqlAnnotationNames.CollationDelegation] as DelegationModes?; return new MethodCallCodeFragment( _modelUseCollationMethodInfo, - new[] {annotation.Value} + new[] { annotation.Value } .AppendIfTrue(delegationModes.HasValue, delegationModes) .ToArray()); } @@ -146,7 +167,7 @@ protected override MethodCallCodeFragment GenerateFluentApi(IEntityType entityTy var delegationModes = entityType[MySqlAnnotationNames.CharSetDelegation] as DelegationModes?; return new MethodCallCodeFragment( _entityTypeHasCharSetMethodInfo, - new[] {annotation.Value} + new[] { annotation.Value } .AppendIfTrue(delegationModes.HasValue, delegationModes) .ToArray()); } @@ -165,7 +186,7 @@ protected override MethodCallCodeFragment GenerateFluentApi(IEntityType entityTy var delegationModes = entityType[MySqlAnnotationNames.CollationDelegation] as DelegationModes?; return new MethodCallCodeFragment( _entityTypeUseCollationMethodInfo, - new[] {annotation.Value} + new[] { annotation.Value } .AppendIfTrue(delegationModes.HasValue, delegationModes) .ToArray()); } @@ -192,7 +213,7 @@ protected override AttributeCodeFragment GenerateDataAnnotation(IEntityType enti var delegationModes = entityType[MySqlAnnotationNames.CharSetDelegation] as DelegationModes?; return new AttributeCodeFragment( typeof(MySqlCharSetAttribute), - new[] {annotation.Value} + new[] { annotation.Value } .AppendIfTrue(delegationModes.HasValue, delegationModes) .ToArray()); } @@ -211,7 +232,7 @@ protected override AttributeCodeFragment GenerateDataAnnotation(IEntityType enti var delegationModes = entityType[MySqlAnnotationNames.CollationDelegation] as DelegationModes?; return new AttributeCodeFragment( typeof(MySqlCollationAttribute), - new[] {annotation.Value} + new[] { annotation.Value } .AppendIfTrue(delegationModes.HasValue, delegationModes) .ToArray()); } @@ -240,7 +261,7 @@ protected override MethodCallCodeFragment GenerateFluentApi(IProperty property, switch (annotation.Name) { - case MySqlAnnotationNames.CharSet when annotation.Value is string {Length: > 0} charSet: + case MySqlAnnotationNames.CharSet when annotation.Value is string { Length: > 0 } charSet: return new MethodCallCodeFragment( _propertyHasCharSetMethodInfo, charSet); @@ -257,10 +278,74 @@ protected override AttributeCodeFragment GenerateDataAnnotation(IProperty proper return annotation.Name switch { - MySqlAnnotationNames.CharSet when annotation.Value is string {Length: > 0} charSet => new AttributeCodeFragment(typeof(MySqlCharSetAttribute), charSet), - RelationalAnnotationNames.Collation when annotation.Value is string {Length: > 0} collation => new AttributeCodeFragment(typeof(MySqlCollationAttribute), collation), + MySqlAnnotationNames.CharSet when annotation.Value is string { Length: > 0 } charSet => new AttributeCodeFragment( + typeof(MySqlCharSetAttribute), charSet), + RelationalAnnotationNames.Collation when annotation.Value is string { Length: > 0 } collation => new AttributeCodeFragment( + typeof(MySqlCollationAttribute), collation), _ => base.GenerateDataAnnotation(property, annotation) }; } + + public override IReadOnlyList GenerateFluentApiCalls( + IModel model, + IDictionary annotations) + { + var fragments = new List(base.GenerateFluentApiCalls(model, annotations)); + + if (GenerateValueGenerationStrategy(annotations, onModel: true) is { } valueGenerationStrategy) + { + fragments.Add(valueGenerationStrategy); + } + + return fragments; + } + + public override IReadOnlyList GenerateFluentApiCalls( + IProperty property, + IDictionary annotations) + { + var fragments = new List(base.GenerateFluentApiCalls(property, annotations)); + + if (GenerateValueGenerationStrategy(annotations, onModel: false) is { } valueGenerationStrategy) + { + fragments.Add(valueGenerationStrategy); + } + + return fragments; + } + + private MethodCallCodeFragment GenerateValueGenerationStrategy(IDictionary annotations, bool onModel) + => TryGetAndRemove(annotations, MySqlAnnotationNames.ValueGenerationStrategy, out MySqlValueGenerationStrategy strategy) + ? strategy switch + { + MySqlValueGenerationStrategy.IdentityColumn => new MethodCallCodeFragment( + onModel + ? _modelUseIdentityColumnsMethodInfo + : _propertyUseIdentityColumnMethodInfo), + MySqlValueGenerationStrategy.ComputedColumn => new MethodCallCodeFragment(_propertyUseComputedColumnMethodInfo), + MySqlValueGenerationStrategy.None => new MethodCallCodeFragment( + _modelHasAnnotationMethodInfo, + MySqlAnnotationNames.ValueGenerationStrategy, + MySqlValueGenerationStrategy.None), + _ => throw new ArgumentOutOfRangeException(strategy.ToString()) + } + : null; + + private static bool TryGetAndRemove( + IDictionary annotations, + string annotationName, + [NotNullWhen(true)] out T annotationValue) + { + if (annotations.TryGetValue(annotationName, out var annotation) + && annotation.Value is not null) + { + annotations.Remove(annotationName); + annotationValue = (T)annotation.Value; + return true; + } + + annotationValue = default; + return false; + } } } diff --git a/src/EFCore.MySql/Design/Internal/MySqlCSharpRuntimeAnnotationCodeGenerator.cs b/src/EFCore.MySql/Design/Internal/MySqlCSharpRuntimeAnnotationCodeGenerator.cs index cf83da09b..494f88a10 100644 --- a/src/EFCore.MySql/Design/Internal/MySqlCSharpRuntimeAnnotationCodeGenerator.cs +++ b/src/EFCore.MySql/Design/Internal/MySqlCSharpRuntimeAnnotationCodeGenerator.cs @@ -1,13 +1,20 @@ // Copyright (c) Pomelo Foundation. All rights reserved. // Licensed under the MIT. See LICENSE in the project root for license information. +using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Design.Internal; +using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage; +using Pomelo.EntityFrameworkCore.MySql.Metadata.Internal; using Pomelo.EntityFrameworkCore.MySql.Storage.Internal; namespace Pomelo.EntityFrameworkCore.MySql.Design.Internal; +// Used to generate a compiled model. The compiled model is only used at app runtime and not for design-time purposes. +// Therefore, all annotations that are related to design-time concerns (i.e. databases, tables or columns) are superfluous and should be +// removed. +// TOOD: Check behavior for `ValueGenerationStrategy`, `LegacyValueGeneratedOnAdd` and `LegacyValueGeneratedOnAddOrUpdate`. public class MySqlCSharpRuntimeAnnotationCodeGenerator : RelationalCSharpRuntimeAnnotationCodeGenerator { public MySqlCSharpRuntimeAnnotationCodeGenerator( @@ -33,4 +40,166 @@ public override bool Create( return result; } + + public override void Generate(IModel model, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) + { + if (!parameters.IsRuntime) + { + var annotations = parameters.Annotations; + + annotations.Remove(MySqlAnnotationNames.CharSet); + annotations.Remove(MySqlAnnotationNames.CharSetDelegation); +#pragma warning disable CS0618 // Type or member is obsolete + annotations.Remove(MySqlAnnotationNames.Collation); +#pragma warning restore CS0618 // Type or member is obsolete + annotations.Remove(MySqlAnnotationNames.CollationDelegation); + annotations.Remove(MySqlAnnotationNames.GuidCollation); + } + + base.Generate(model, parameters); + } + + public override void Generate(IRelationalModel model, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) + { + if (!parameters.IsRuntime) + { + var annotations = parameters.Annotations; + + annotations.Remove(MySqlAnnotationNames.CharSet); + annotations.Remove(MySqlAnnotationNames.CharSetDelegation); +#pragma warning disable CS0618 // Type or member is obsolete + annotations.Remove(MySqlAnnotationNames.Collation); +#pragma warning restore CS0618 // Type or member is obsolete + annotations.Remove(MySqlAnnotationNames.CollationDelegation); + annotations.Remove(MySqlAnnotationNames.GuidCollation); + + annotations.Remove(RelationalAnnotationNames.Collation); + } + + base.Generate(model, parameters); + } + + public override void Generate(IEntityType entityType, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) + { + if (!parameters.IsRuntime) + { + var annotations = parameters.Annotations; + + annotations.Remove(MySqlAnnotationNames.CharSet); + annotations.Remove(MySqlAnnotationNames.CharSetDelegation); +#pragma warning disable CS0618 // Type or member is obsolete + annotations.Remove(MySqlAnnotationNames.Collation); +#pragma warning restore CS0618 // Type or member is obsolete + annotations.Remove(MySqlAnnotationNames.CollationDelegation); + annotations.Remove(MySqlAnnotationNames.StoreOptions); + + annotations.Remove(RelationalAnnotationNames.Collation); + } + + base.Generate(entityType, parameters); + } + + public override void Generate(ITable table, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) + { + if (!parameters.IsRuntime) + { + var annotations = parameters.Annotations; + + annotations.Remove(MySqlAnnotationNames.CharSet); + annotations.Remove(MySqlAnnotationNames.CharSetDelegation); +#pragma warning disable CS0618 // Type or member is obsolete + annotations.Remove(MySqlAnnotationNames.Collation); +#pragma warning restore CS0618 // Type or member is obsolete + annotations.Remove(MySqlAnnotationNames.CollationDelegation); + annotations.Remove(MySqlAnnotationNames.StoreOptions); + + annotations.Remove(RelationalAnnotationNames.Collation); + } + + base.Generate(table, parameters); + } + + public override void Generate(IProperty property, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) + { + if (!parameters.IsRuntime) + { + var annotations = parameters.Annotations; + + annotations.Remove(MySqlAnnotationNames.CharSet); +#pragma warning disable CS0618 // Type or member is obsolete + annotations.Remove(MySqlAnnotationNames.Collation); +#pragma warning restore CS0618 // Type or member is obsolete + annotations.Remove(MySqlAnnotationNames.SpatialReferenceSystemId); + + annotations.Remove(RelationalAnnotationNames.Collation); + + if (!annotations.ContainsKey(MySqlAnnotationNames.ValueGenerationStrategy)) + { + annotations[MySqlAnnotationNames.ValueGenerationStrategy] = property.GetValueGenerationStrategy(); + } + } + + base.Generate(property, parameters); + } + + public override void Generate(IColumn column, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) + { + if (!parameters.IsRuntime) + { + var annotations = parameters.Annotations; + + annotations.Remove(MySqlAnnotationNames.CharSet); +#pragma warning disable CS0618 // Type or member is obsolete + annotations.Remove(MySqlAnnotationNames.Collation); +#pragma warning restore CS0618 // Type or member is obsolete + annotations.Remove(MySqlAnnotationNames.SpatialReferenceSystemId); + annotations.Remove(MySqlAnnotationNames.ValueGenerationStrategy); + + annotations.Remove(RelationalAnnotationNames.Collation); + } + + base.Generate(column, parameters); + } + + public override void Generate(IIndex index, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) + { + if (!parameters.IsRuntime) + { + var annotations = parameters.Annotations; + + annotations.Remove(MySqlAnnotationNames.FullTextIndex); + annotations.Remove(MySqlAnnotationNames.FullTextParser); + annotations.Remove(MySqlAnnotationNames.IndexPrefixLength); + annotations.Remove(MySqlAnnotationNames.SpatialIndex); + } + + base.Generate(index, parameters); + } + + public override void Generate(ITableIndex index, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) + { + if (!parameters.IsRuntime) + { + var annotations = parameters.Annotations; + + annotations.Remove(MySqlAnnotationNames.FullTextIndex); + annotations.Remove(MySqlAnnotationNames.FullTextParser); + annotations.Remove(MySqlAnnotationNames.IndexPrefixLength); + annotations.Remove(MySqlAnnotationNames.SpatialIndex); + } + + base.Generate(index, parameters); + } + + public override void Generate(IKey key, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) + { + if (!parameters.IsRuntime) + { + var annotations = parameters.Annotations; + + annotations.Remove(MySqlAnnotationNames.IndexPrefixLength); + } + + base.Generate(key, parameters); + } } diff --git a/src/EFCore.MySql/Extensions/MySqlEntityTypeExtensions.cs b/src/EFCore.MySql/Extensions/MySqlEntityTypeExtensions.cs index 38601bec9..778905b6f 100644 --- a/src/EFCore.MySql/Extensions/MySqlEntityTypeExtensions.cs +++ b/src/EFCore.MySql/Extensions/MySqlEntityTypeExtensions.cs @@ -6,6 +6,7 @@ using System.Text; using System.Text.RegularExpressions; using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Utilities; using Pomelo.EntityFrameworkCore.MySql.Metadata.Internal; @@ -26,7 +27,9 @@ public static class MySqlEntityTypeExtensions /// The entity type. /// The name of the character set. public static string GetCharSet([NotNull] this IReadOnlyEntityType entityType) - => entityType[MySqlAnnotationNames.CharSet] as string; + => (entityType is RuntimeEntityType) + ? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData) + : entityType[MySqlAnnotationNames.CharSet] as string; /// /// Sets the MySQL character set on the table associated with this entity. When you only specify the character set, MySQL implicitly @@ -81,12 +84,14 @@ public static string SetCharSet( /// The entity type. /// The character set delegation modes. public static DelegationModes? GetCharSetDelegation([NotNull] this IReadOnlyEntityType entityType) - => ObjectToEnumConverter.GetEnumValue(entityType[MySqlAnnotationNames.CharSetDelegation]) ?? - (entityType[MySqlAnnotationNames.CharSetDelegation] is bool explicitlyDelegateToChildren - ? explicitlyDelegateToChildren - ? DelegationModes.ApplyToAll - : DelegationModes.ApplyToDatabases - : null); + => (entityType is RuntimeEntityType) + ? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData) + : ObjectToEnumConverter.GetEnumValue(entityType[MySqlAnnotationNames.CharSetDelegation]) ?? + (entityType[MySqlAnnotationNames.CharSetDelegation] is bool explicitlyDelegateToChildren + ? explicitlyDelegateToChildren + ? DelegationModes.ApplyToAll + : DelegationModes.ApplyToDatabases + : null); /// /// Attempts to set the character set delegation modes for entity/table. @@ -153,7 +158,9 @@ public static DelegationModes GetActualCharSetDelegation([NotNull] this IReadOnl /// The entity type. /// The name of the collation. public static string GetCollation([NotNull] this IReadOnlyEntityType entityType) - => entityType[RelationalAnnotationNames.Collation] as string; + => (entityType is RuntimeEntityType) + ? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData) + : entityType[RelationalAnnotationNames.Collation] as string; /// /// Sets the MySQL collation on the table associated with this entity. When you specify the collation, MySQL implicitly sets the @@ -208,12 +215,14 @@ public static string SetCollation( /// The entity type. /// The collation delegation modes. public static DelegationModes? GetCollationDelegation([NotNull] this IReadOnlyEntityType entityType) - => ObjectToEnumConverter.GetEnumValue(entityType[MySqlAnnotationNames.CollationDelegation]) ?? - (entityType[MySqlAnnotationNames.CollationDelegation] is bool explicitlyDelegateToChildren - ? explicitlyDelegateToChildren - ? DelegationModes.ApplyToAll - : DelegationModes.ApplyToDatabases - : null); + => (entityType is RuntimeEntityType) + ? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData) + : ObjectToEnumConverter.GetEnumValue(entityType[MySqlAnnotationNames.CollationDelegation]) ?? + (entityType[MySqlAnnotationNames.CollationDelegation] is bool explicitlyDelegateToChildren + ? explicitlyDelegateToChildren + ? DelegationModes.ApplyToAll + : DelegationModes.ApplyToDatabases + : null); /// /// Attempts to set the collation delegation modes for entity/table. @@ -280,7 +289,9 @@ public static DelegationModes GetActualCollationDelegation([NotNull] this IReadO /// The entity type. /// A dictionary of table options. public static Dictionary GetTableOptions([NotNull] this IReadOnlyEntityType entityType) - => DeserializeTableOptions(entityType[MySqlAnnotationNames.StoreOptions] as string); + => (entityType is RuntimeEntityType) + ? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData) + : DeserializeTableOptions(entityType[MySqlAnnotationNames.StoreOptions] as string); /// /// Sets the MySQL table options for the table associated with this entity. diff --git a/src/EFCore.MySql/Extensions/MySqlIndexExtensions.cs b/src/EFCore.MySql/Extensions/MySqlIndexExtensions.cs index e107c8be8..a585d4bc4 100644 --- a/src/EFCore.MySql/Extensions/MySqlIndexExtensions.cs +++ b/src/EFCore.MySql/Extensions/MySqlIndexExtensions.cs @@ -1,7 +1,9 @@ // Copyright (c) Pomelo Foundation. All rights reserved. // Licensed under the MIT. See LICENSE in the project root for license information. +using System; using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Metadata; using Pomelo.EntityFrameworkCore.MySql.Metadata.Internal; @@ -19,7 +21,9 @@ public static class MySqlIndexExtensions /// The index. /// if the index is full text. public static bool? IsFullText([NotNull] this IIndex index) - => (bool?)index[MySqlAnnotationNames.FullTextIndex]; + => (index is RuntimeIndex) + ? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData) + : (bool?)index[MySqlAnnotationNames.FullTextIndex]; /// /// Sets a value indicating whether the index is full text. @@ -58,7 +62,9 @@ public static void SetIsFullText([NotNull] this IMutableIndex index, bool? value /// The index. /// The name of the full text parser. [CanBeNull] public static string FullTextParser([NotNull] this IIndex index) - => (string)index[MySqlAnnotationNames.FullTextParser]; + => (index is RuntimeIndex) + ? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData) + : (string)index[MySqlAnnotationNames.FullTextParser]; /// /// Sets a value indicating which full text parser to used. @@ -98,7 +104,9 @@ public static string SetFullTextParser([NotNull] this IConventionIndex index, [C /// The prefix lengths. /// A value of `0` indicates, that the full length should be used for that column. public static int[] PrefixLength([NotNull] this IIndex index) - => (int[])index[MySqlAnnotationNames.IndexPrefixLength]; + => (index is RuntimeIndex) + ? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData) + : (int[])index[MySqlAnnotationNames.IndexPrefixLength]; /// /// Sets prefix lengths for the index. @@ -139,7 +147,9 @@ public static int[] SetPrefixLength([NotNull] this IConventionIndex index, int[] /// The index. /// if the index is spartial. public static bool? IsSpatial([NotNull] this IIndex index) - => (bool?)index[MySqlAnnotationNames.SpatialIndex]; + => (index is RuntimeIndex) + ? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData) + : (bool?)index[MySqlAnnotationNames.SpatialIndex]; /// /// Sets a value indicating whether the index is spartial. diff --git a/src/EFCore.MySql/Extensions/MySqlKeyExtensions.cs b/src/EFCore.MySql/Extensions/MySqlKeyExtensions.cs index e0f7e9069..da57fa43a 100644 --- a/src/EFCore.MySql/Extensions/MySqlKeyExtensions.cs +++ b/src/EFCore.MySql/Extensions/MySqlKeyExtensions.cs @@ -1,7 +1,9 @@ // Copyright (c) Pomelo Foundation. All rights reserved. // Licensed under the MIT. See LICENSE in the project root for license information. +using System; using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Metadata; using Pomelo.EntityFrameworkCore.MySql.Metadata.Internal; @@ -20,7 +22,9 @@ public static class MySqlKeyExtensions /// The prefix lengths. /// A value of `0` indicates, that the full length should be used for that column. public static int[] PrefixLength([NotNull] this IKey key) - => (int[])key[MySqlAnnotationNames.IndexPrefixLength]; + => (key is RuntimeKey) + ? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData) + : (int[])key[MySqlAnnotationNames.IndexPrefixLength]; /// /// Sets prefix lengths for the key. diff --git a/src/EFCore.MySql/Extensions/MySqlModelBuilderExtensions.cs b/src/EFCore.MySql/Extensions/MySqlModelBuilderExtensions.cs index 7864e31c0..d6a484a10 100644 --- a/src/EFCore.MySql/Extensions/MySqlModelBuilderExtensions.cs +++ b/src/EFCore.MySql/Extensions/MySqlModelBuilderExtensions.cs @@ -6,7 +6,6 @@ using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Utilities; -using Pomelo.EntityFrameworkCore.MySql.Infrastructure; using Pomelo.EntityFrameworkCore.MySql.Metadata.Internal; // ReSharper disable once CheckNamespace @@ -14,6 +13,69 @@ namespace Microsoft.EntityFrameworkCore { public static class MySqlModelBuilderExtensions { + #region AutoIncrement + + /// + /// Configures the model to use the AUTO_INCREMENT feature to generate values for properties + /// marked as , when targeting MySQL. + /// + /// The model builder. + /// The same builder instance so that multiple calls can be chained. + public static ModelBuilder AutoIncrementColumns(this ModelBuilder modelBuilder) + { + Check.NotNull(modelBuilder, nameof(modelBuilder)); + + var model = modelBuilder.Model; + + model.SetValueGenerationStrategy(MySqlValueGenerationStrategy.IdentityColumn); + + return modelBuilder; + } + + /// + /// Configures the value generation strategy for the key property, when targeting MySQL. + /// + /// The builder for the property being configured. + /// The value generation strategy. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, otherwise. + /// + public static IConventionModelBuilder HasValueGenerationStrategy( + this IConventionModelBuilder modelBuilder, + MySqlValueGenerationStrategy? valueGenerationStrategy, + bool fromDataAnnotation = false) + { + if (modelBuilder.CanSetValueGenerationStrategy(valueGenerationStrategy, fromDataAnnotation)) + { + modelBuilder.Metadata.SetValueGenerationStrategy(valueGenerationStrategy, fromDataAnnotation); + + return modelBuilder; + } + + return null; + } + + /// + /// Returns a value indicating whether the given value can be set as the default value generation strategy. + /// + /// The model builder. + /// The value generation strategy. + /// Indicates whether the configuration was specified using a data annotation. + /// if the given value can be set as the default value generation strategy. + public static bool CanSetValueGenerationStrategy( + this IConventionModelBuilder modelBuilder, + MySqlValueGenerationStrategy? valueGenerationStrategy, + bool fromDataAnnotation = false) + { + Check.NotNull(modelBuilder, nameof(modelBuilder)); + + return modelBuilder.CanSetAnnotation( + MySqlAnnotationNames.ValueGenerationStrategy, valueGenerationStrategy, fromDataAnnotation); + } + + #endregion Identity + #region CharSet and delegation /// diff --git a/src/EFCore.MySql/Extensions/MySqlModelExtensions.cs b/src/EFCore.MySql/Extensions/MySqlModelExtensions.cs index cffb119dc..739ba1394 100644 --- a/src/EFCore.MySql/Extensions/MySqlModelExtensions.cs +++ b/src/EFCore.MySql/Extensions/MySqlModelExtensions.cs @@ -3,6 +3,7 @@ using System; using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Metadata; using Pomelo.EntityFrameworkCore.MySql.Metadata.Internal; @@ -20,17 +21,11 @@ public static class MySqlModelExtensions /// The model. /// The default . public static MySqlValueGenerationStrategy? GetValueGenerationStrategy([NotNull] this IReadOnlyModel model) - { - // Allow users to use the underlying type value instead of the enum itself. - // Workaround for: https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql/issues/1205 - if (model[MySqlAnnotationNames.ValueGenerationStrategy] is { } annotation && - ObjectToEnumConverter.GetEnumValue(annotation) is { } enumValue) - { - return enumValue; - } - - return null; - } + => model[MySqlAnnotationNames.ValueGenerationStrategy] is { } annotationValue + ? ObjectToEnumConverter.GetEnumValue(annotationValue) is { } enumValue + ? enumValue + : (MySqlValueGenerationStrategy)annotationValue + : null; /// /// Attempts to set the to use for properties @@ -74,7 +69,9 @@ public static void SetValueGenerationStrategy([NotNull] this IMutableModel model /// The model. /// The default character set. public static string GetCharSet([NotNull] this IReadOnlyModel model) - => model[MySqlAnnotationNames.CharSet] as string; + => (model is RuntimeModel) + ? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData) + : model[MySqlAnnotationNames.CharSet] as string; /// /// Attempts to set the character set to use as the default for the model/database. @@ -115,12 +112,14 @@ public static string SetCharSet([NotNull] this IConventionModel model, string ch /// The model. /// The character set delegation modes. public static DelegationModes? GetCharSetDelegation([NotNull] this IReadOnlyModel model) - => ObjectToEnumConverter.GetEnumValue(model[MySqlAnnotationNames.CharSetDelegation]) ?? - (model[MySqlAnnotationNames.CharSetDelegation] is bool explicitlyDelegateToChildren - ? explicitlyDelegateToChildren - ? DelegationModes.ApplyToAll - : DelegationModes.ApplyToDatabases - : null); + => (model is RuntimeModel) + ? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData) + : ObjectToEnumConverter.GetEnumValue(model[MySqlAnnotationNames.CharSetDelegation]) ?? + (model[MySqlAnnotationNames.CharSetDelegation] is bool explicitlyDelegateToChildren + ? explicitlyDelegateToChildren + ? DelegationModes.ApplyToAll + : DelegationModes.ApplyToDatabases + : null); /// /// Attempts to set the character set delegation modes for the model/database. @@ -181,12 +180,14 @@ public static DelegationModes GetActualCharSetDelegation([NotNull] this IReadOnl /// The model. /// The collation delegation modes. public static DelegationModes? GetCollationDelegation([NotNull] this IReadOnlyModel model) - => ObjectToEnumConverter.GetEnumValue(model[MySqlAnnotationNames.CollationDelegation]) ?? - (model[MySqlAnnotationNames.CollationDelegation] is bool explicitlyDelegateToChildren - ? explicitlyDelegateToChildren - ? DelegationModes.ApplyToAll - : DelegationModes.ApplyToDatabases - : null); + => (model is RuntimeModel) + ? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData) + : ObjectToEnumConverter.GetEnumValue(model[MySqlAnnotationNames.CollationDelegation]) ?? + (model[MySqlAnnotationNames.CollationDelegation] is bool explicitlyDelegateToChildren + ? explicitlyDelegateToChildren + ? DelegationModes.ApplyToAll + : DelegationModes.ApplyToDatabases + : null); /// /// Attempts to set the collation delegation modes for the model/database. @@ -251,7 +252,9 @@ public static DelegationModes GetActualCollationDelegation([NotNull] this IReadO /// collation `ascii_general_ci` will be applied. /// public static string GetGuidCollation([NotNull] this IReadOnlyModel model) - => model[MySqlAnnotationNames.GuidCollation] as string; + => (model is RuntimeModel) + ? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData) + : model[MySqlAnnotationNames.GuidCollation] as string; /// /// Attempts to set the default collation used for char-based columns. diff --git a/src/EFCore.MySql/Extensions/MySqlPropertyBuilderExtensions.cs b/src/EFCore.MySql/Extensions/MySqlPropertyBuilderExtensions.cs index b840c6216..2377a6e94 100644 --- a/src/EFCore.MySql/Extensions/MySqlPropertyBuilderExtensions.cs +++ b/src/EFCore.MySql/Extensions/MySqlPropertyBuilderExtensions.cs @@ -6,7 +6,6 @@ using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Utilities; -using Pomelo.EntityFrameworkCore.MySql.Infrastructure; using Pomelo.EntityFrameworkCore.MySql.Metadata.Internal; // ReSharper disable once CheckNamespace @@ -17,13 +16,15 @@ namespace Microsoft.EntityFrameworkCore /// public static class MySqlPropertyBuilderExtensions { + #region AutoIncrement + /// - /// Configures the key property to use the MySQL IDENTITY feature to generate values for new entities, + /// Configures the key property to use the AUTO_INCREMENT feature to generate values for new entities, /// when targeting MySQL. This method sets the property to be . /// /// The builder for the property being configured. /// The same builder instance so that multiple calls can be chained. - public static PropertyBuilder UseMySqlIdentityColumn( + public static PropertyBuilder UseMySqlIdentityColumn( // TODO: Mark as obsolete and introduce UseAutoIncrementColumn instead. [NotNull] this PropertyBuilder propertyBuilder) { Check.NotNull(propertyBuilder, nameof(propertyBuilder)); @@ -35,16 +36,69 @@ public static PropertyBuilder UseMySqlIdentityColumn( } /// - /// Configures the key property to use the MySQL IDENTITY feature to generate values for new entities, + /// Configures the key property to use the AUTO_INCREMENT feature to generate values for new entities, /// when targeting MySQL. This method sets the property to be . /// /// The type of the property being configured. /// The builder for the property being configured. /// The same builder instance so that multiple calls can be chained. - public static PropertyBuilder UseMySqlIdentityColumn( + public static PropertyBuilder UseMySqlIdentityColumn( // TODO: Mark as obsolete and introduce UseAutoIncrementColumn instead. [NotNull] this PropertyBuilder propertyBuilder) => (PropertyBuilder)UseMySqlIdentityColumn((PropertyBuilder)propertyBuilder); + #endregion Identity + + #region General value generation strategy + + /// + /// Configures the value generation strategy for the key property, when targeting MySQL. + /// + /// The builder for the property being configured. + /// The value generation strategy. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, otherwise. + /// + public static IConventionPropertyBuilder HasValueGenerationStrategy( + this IConventionPropertyBuilder propertyBuilder, + MySqlValueGenerationStrategy? valueGenerationStrategy, + bool fromDataAnnotation = false) + { + if (propertyBuilder.CanSetAnnotation( + MySqlAnnotationNames.ValueGenerationStrategy, valueGenerationStrategy, fromDataAnnotation)) + { + propertyBuilder.Metadata.SetValueGenerationStrategy(valueGenerationStrategy, fromDataAnnotation); + + return propertyBuilder; + } + + return null; + } + + /// + /// Returns a value indicating whether the given value can be set as the value generation strategy. + /// + /// The builder for the property being configured. + /// The value generation strategy. + /// Indicates whether the configuration was specified using a data annotation. + /// if the given value can be set as the default value generation strategy. + public static bool CanSetValueGenerationStrategy( + this IConventionPropertyBuilder propertyBuilder, + MySqlValueGenerationStrategy? valueGenerationStrategy, + bool fromDataAnnotation = false) + { + Check.NotNull(propertyBuilder, nameof(propertyBuilder)); + + return (valueGenerationStrategy is null || + MySqlPropertyExtensions.IsCompatibleIdentityColumn(propertyBuilder.Metadata)) + && propertyBuilder.CanSetAnnotation( + MySqlAnnotationNames.ValueGenerationStrategy, valueGenerationStrategy, fromDataAnnotation); + } + + #endregion General value generation strategy + + #region Computed + /// /// Configures the key property to use the MySQL Computed feature to generate values for new entities, /// when targeting MySQL. This method sets the property to be . @@ -73,6 +127,10 @@ public static PropertyBuilder UseMySqlComputedColumn( [NotNull] this PropertyBuilder propertyBuilder) => (PropertyBuilder)UseMySqlComputedColumn((PropertyBuilder)propertyBuilder); + #endregion Computed + + #region CharSet + /// /// Configures the charset for the property's column. /// @@ -142,6 +200,10 @@ public static bool CanSetCharSet( charSet, fromDataAnnotation); + #endregion CharSet + + #region Collation + /// /// Configures the collation for the property's column. /// @@ -170,6 +232,10 @@ public static PropertyBuilder HasCollation( string collation) => (PropertyBuilder)HasCollation((PropertyBuilder)propertyBuilder, collation); + #endregion Collation + + #region SRID + /// /// Restricts the Spatial Reference System Identifier (SRID) for the property's column. /// @@ -198,5 +264,7 @@ public static PropertyBuilder HasSpatialReferenceSystem( [NotNull] this PropertyBuilder propertyBuilder, int? srid) => (PropertyBuilder)HasSpatialReferenceSystem((PropertyBuilder)propertyBuilder, srid); + + #endregion SRID } } diff --git a/src/EFCore.MySql/Extensions/MySqlPropertyExtensions.cs b/src/EFCore.MySql/Extensions/MySqlPropertyExtensions.cs index 835b9014b..7a7702bb7 100644 --- a/src/EFCore.MySql/Extensions/MySqlPropertyExtensions.cs +++ b/src/EFCore.MySql/Extensions/MySqlPropertyExtensions.cs @@ -2,9 +2,9 @@ // Licensed under the MIT. See LICENSE in the project root for license information. using System; -using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage; @@ -32,12 +32,21 @@ public static class MySqlPropertyExtensions /// The strategy, or if none was set. public static MySqlValueGenerationStrategy GetValueGenerationStrategy([NotNull] this IReadOnlyProperty property) { - // Allow users to use the underlying type value instead of the enum itself. - // Workaround for: https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql/issues/1205 - if (property[MySqlAnnotationNames.ValueGenerationStrategy] is { } annotationValue && - ObjectToEnumConverter.GetEnumValue(annotationValue) is { } enumValue) + if (property.FindAnnotation(MySqlAnnotationNames.ValueGenerationStrategy) is { } annotation) { - return enumValue; + if (annotation.Value is { } annotationValue) + { + // Allow users to use the underlying type value instead of the enum itself. + // Workaround for: https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql/issues/1205 + if (ObjectToEnumConverter.GetEnumValue(annotationValue) is { } enumValue) + { + return enumValue; + } + + return (MySqlValueGenerationStrategy)annotationValue; + } + + return MySqlValueGenerationStrategy.None; } if (property.ValueGenerated == ValueGenerated.OnAdd) @@ -50,11 +59,6 @@ public static MySqlValueGenerationStrategy GetValueGenerationStrategy([NotNull] return MySqlValueGenerationStrategy.None; } - if (IsCompatibleIdentityColumn(property)) - { - return MySqlValueGenerationStrategy.IdentityColumn; - } - return GetDefaultValueGenerationStrategy(property); } @@ -126,11 +130,6 @@ is StoreObjectIdentifier principal return MySqlValueGenerationStrategy.None; } - if (IsCompatibleIdentityColumn(property)) - { - return MySqlValueGenerationStrategy.IdentityColumn; - } - var defaultStrategy = GetDefaultValueGenerationStrategy(property, storeObject, typeMappingSource); if (defaultStrategy != MySqlValueGenerationStrategy.None) { @@ -172,13 +171,10 @@ private static MySqlValueGenerationStrategy GetDefaultValueGenerationStrategy(IR { var modelStrategy = property.DeclaringType.Model.GetValueGenerationStrategy(); - if (modelStrategy == MySqlValueGenerationStrategy.IdentityColumn && - IsCompatibleAutoIncrementColumn(property)) - { - return MySqlValueGenerationStrategy.IdentityColumn; - } - - return MySqlValueGenerationStrategy.None; + return modelStrategy == MySqlValueGenerationStrategy.IdentityColumn && + IsCompatibleIdentityColumn(property) + ? MySqlValueGenerationStrategy.IdentityColumn + : MySqlValueGenerationStrategy.None; } private static MySqlValueGenerationStrategy GetDefaultValueGenerationStrategy( @@ -189,7 +185,7 @@ private static MySqlValueGenerationStrategy GetDefaultValueGenerationStrategy( var modelStrategy = property.DeclaringType.Model.GetValueGenerationStrategy(); return modelStrategy == MySqlValueGenerationStrategy.IdentityColumn - && IsCompatibleAutoIncrementColumn(property, storeObject, typeMappingSource) + && IsCompatibleIdentityColumn(property, storeObject, typeMappingSource) ? MySqlValueGenerationStrategy.IdentityColumn : MySqlValueGenerationStrategy.None; } @@ -359,9 +355,12 @@ private static bool IsCompatibleIdentityColumn( /// if compatible. public static bool IsCompatibleAutoIncrementColumn(IReadOnlyProperty property) { - var valueConverter = GetConverter(property); + var valueConverter = property.GetValueConverter() ?? + property.FindTypeMapping()?.Converter; + var type = (valueConverter?.ProviderClrType ?? property.ClrType).UnwrapNullableType(); return type.IsInteger() || + type.IsEnum || type == typeof(decimal); } @@ -375,11 +374,15 @@ private static bool IsCompatibleAutoIncrementColumn( return false; } - var valueConverter = GetConverter(property, storeObject, typeMappingSource); + var valueConverter = property.GetValueConverter() ?? + (property.FindRelationalTypeMapping(storeObject) ?? + typeMappingSource?.FindMapping((IProperty)property))?.Converter; + var type = (valueConverter?.ProviderClrType ?? property.ClrType).UnwrapNullableType(); - return (type.IsInteger() - || type == typeof(decimal)); + return (type.IsInteger() || + type.IsEnum || + type == typeof(decimal)); } /// @@ -453,8 +456,24 @@ private static ValueConverter GetConverter( /// The property of which to get the columns charset from. /// The name of the charset or null, if no explicit charset was set. public static string GetCharSet([NotNull] this IReadOnlyProperty property) - => property[MySqlAnnotationNames.CharSet] as string ?? - property.GetMySqlLegacyCharSet(); + => (property is RuntimeProperty) + ? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData) + : property[MySqlAnnotationNames.CharSet] as string ?? + property.GetMySqlLegacyCharSet(); + + /// + /// Returns the name of the charset used by the column of the property. + /// + /// The property of which to get the columns charset from. + /// The identifier of the table-like store object containing the column. + /// The name of the charset or null, if no explicit charset was set. + public static string GetCharSet(this IReadOnlyProperty property, in StoreObjectIdentifier storeObject) + => property is RuntimeProperty + ? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData) + : property.FindAnnotation(MySqlAnnotationNames.CharSet) is { } annotation + ? annotation.Value as string ?? + property.GetMySqlLegacyCharSet() + : property.FindSharedStoreObjectRootProperty(storeObject)?.GetCharSet(storeObject); /// /// Returns the name of the charset used by the column of the property, defined as part of the column type. @@ -545,7 +564,22 @@ internal static string GetMySqlLegacyCollation([NotNull] this IReadOnlyProperty /// The property of which to get the columns SRID from. /// The SRID or null, if no explicit SRID has been set. public static int? GetSpatialReferenceSystem([NotNull] this IReadOnlyProperty property) - => (int?)property[MySqlAnnotationNames.SpatialReferenceSystemId]; + => (property is RuntimeProperty) + ? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData) + : (int?)property[MySqlAnnotationNames.SpatialReferenceSystemId]; + + /// + /// Returns the Spatial Reference System Identifier (SRID) used by the column of the property. + /// + /// The property of which to get the columns SRID from. + /// The identifier of the table-like store object containing the column. + /// The SRID or null, if no explicit SRID has been set. + public static int? GetSpatialReferenceSystem(this IReadOnlyProperty property, in StoreObjectIdentifier storeObject) + => property is RuntimeProperty + ? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData) + : property.FindAnnotation(MySqlAnnotationNames.SpatialReferenceSystemId) is { } annotation + ? (int?)annotation.Value + : property.FindSharedStoreObjectRootProperty(storeObject)?.GetSpatialReferenceSystem(storeObject); /// /// Sets the Spatial Reference System Identifier (SRID) in use by the column of the property. diff --git a/src/EFCore.MySql/Infrastructure/MySqlServerVersion.cs b/src/EFCore.MySql/Infrastructure/MySqlServerVersion.cs index e267b8e55..04062afa9 100644 --- a/src/EFCore.MySql/Infrastructure/MySqlServerVersion.cs +++ b/src/EFCore.MySql/Infrastructure/MySqlServerVersion.cs @@ -80,7 +80,7 @@ internal MySqlServerVersionSupport([NotNull] ServerVersion serverVersion) public override bool JsonDataTypeEmulation => false; public override bool ImplicitBoolCheckUsesIndex => ServerVersion.Version >= new Version(8, 0, 0); // Exact version has not been verified yet public override bool MySqlBug96947Workaround => ServerVersion.Version >= new Version(5, 7, 0) && - ServerVersion.Version < new Version(8, 0, 25); // Exact version has not been verified yet, but it is 5.7.x and could very well be 5.7.0 + ServerVersion.Version < new Version(8, 0, 23); // Exact version has not been verified yet, but it is 5.7.x and could very well be 5.7.0 public override bool MySqlBug104294Workaround => ServerVersion.Version >= new Version(8, 0, 0); // Exact version has not been determined yet public override bool FullTextParser => ServerVersion.Version >= new Version(5, 7, 3); public override bool InformationSchemaCheckConstraintsTable => ServerVersion.Version >= new Version(8, 0, 16); // MySQL is missing the explicit TABLE_NAME column that MariaDB supports, so always join the TABLE_CONSTRAINTS table when accessing CHECK_CONSTRAINTS for any database server that supports CHECK_CONSTRAINTS. diff --git a/src/EFCore.MySql/Metadata/Conventions/MySqlConventionSetBuilder.cs b/src/EFCore.MySql/Metadata/Conventions/MySqlConventionSetBuilder.cs index 2a32c0a5f..aa0f1444c 100644 --- a/src/EFCore.MySql/Metadata/Conventions/MySqlConventionSetBuilder.cs +++ b/src/EFCore.MySql/Metadata/Conventions/MySqlConventionSetBuilder.cs @@ -35,6 +35,8 @@ public override ConventionSet CreateConventionSet() { var conventionSet = base.CreateConventionSet(); + conventionSet.Add(new MySqlValueGenerationStrategyConvention(Dependencies, RelationalDependencies)); + conventionSet.ModelInitializedConventions.Add(new RelationalMaxIdentifierLengthConvention(64, Dependencies, RelationalDependencies)); conventionSet.EntityTypeAddedConventions.Add(new TableCharSetAttributeConvention(Dependencies)); @@ -50,6 +52,10 @@ public override ConventionSet CreateConventionSet() ReplaceConvention(conventionSet.ForeignKeyRemovedConventions, valueGenerationConvention); conventionSet.PropertyAnnotationChangedConventions.Add(valueGenerationConvention); + ReplaceConvention( + conventionSet.ModelFinalizedConventions, + (RuntimeModelConvention)new MySqlRuntimeModelConvention(Dependencies, RelationalDependencies)); + return conventionSet; } diff --git a/src/EFCore.MySql/Metadata/Conventions/MySqlRuntimeModelConvention.cs b/src/EFCore.MySql/Metadata/Conventions/MySqlRuntimeModelConvention.cs new file mode 100644 index 000000000..c6181687a --- /dev/null +++ b/src/EFCore.MySql/Metadata/Conventions/MySqlRuntimeModelConvention.cs @@ -0,0 +1,133 @@ +// Copyright (c) Pomelo Foundation. All rights reserved. +// Licensed under the MIT. See LICENSE in the project root for license information. + +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Conventions; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; +using Pomelo.EntityFrameworkCore.MySql.Metadata.Internal; + +namespace Pomelo.EntityFrameworkCore.MySql.Metadata.Conventions; + +/// +/// A convention that creates an optimized copy of the mutable model. +/// The runtime model is only used at app runtime and not for design-time purposes. +/// Therefore, all annotations that are related to design-time concerns (i.e. databases, tables or columns) are superfluous and should +/// be removed. +/// +public class MySqlRuntimeModelConvention : RelationalRuntimeModelConvention +{ + /// + /// Creates a new instance of . + /// + /// Parameter object containing dependencies for this convention. + /// Parameter object containing relational dependencies for this convention. + public MySqlRuntimeModelConvention( + ProviderConventionSetBuilderDependencies dependencies, + RelationalConventionSetBuilderDependencies relationalDependencies) + : base(dependencies, relationalDependencies) + { + } + + /// + protected override void ProcessModelAnnotations( + Dictionary annotations, + IModel model, + RuntimeModel runtimeModel, + bool runtime) + { + base.ProcessModelAnnotations(annotations, model, runtimeModel, runtime); + + if (!runtime) + { + annotations.Remove(MySqlAnnotationNames.CharSet); + annotations.Remove(MySqlAnnotationNames.CharSetDelegation); +#pragma warning disable CS0618 // Type or member is obsolete + annotations.Remove(MySqlAnnotationNames.Collation); +#pragma warning restore CS0618 // Type or member is obsolete + annotations.Remove(MySqlAnnotationNames.CollationDelegation); + annotations.Remove(MySqlAnnotationNames.GuidCollation); + } + } + + /// + protected override void ProcessEntityTypeAnnotations( + Dictionary annotations, + IEntityType entityType, + RuntimeEntityType runtimeEntityType, + bool runtime) + { + base.ProcessEntityTypeAnnotations(annotations, entityType, runtimeEntityType, runtime); + + if (!runtime) + { + annotations.Remove(MySqlAnnotationNames.CharSet); + annotations.Remove(MySqlAnnotationNames.CharSetDelegation); +#pragma warning disable CS0618 // Type or member is obsolete + annotations.Remove(MySqlAnnotationNames.Collation); +#pragma warning restore CS0618 // Type or member is obsolete + annotations.Remove(MySqlAnnotationNames.CollationDelegation); + annotations.Remove(MySqlAnnotationNames.StoreOptions); + + annotations.Remove(RelationalAnnotationNames.Collation); + } + } + + /// + protected override void ProcessPropertyAnnotations( + Dictionary annotations, + IProperty property, + RuntimeProperty runtimeProperty, + bool runtime) + { + base.ProcessPropertyAnnotations(annotations, property, runtimeProperty, runtime); + + if (!runtime) + { + annotations.Remove(MySqlAnnotationNames.CharSet); +#pragma warning disable CS0618 // Type or member is obsolete + annotations.Remove(MySqlAnnotationNames.Collation); +#pragma warning restore CS0618 // Type or member is obsolete + annotations.Remove(MySqlAnnotationNames.SpatialReferenceSystemId); + + if (!annotations.ContainsKey(MySqlAnnotationNames.ValueGenerationStrategy)) + { + annotations[MySqlAnnotationNames.ValueGenerationStrategy] = property.GetValueGenerationStrategy(); + } + } + } + + /// + protected override void ProcessIndexAnnotations( + Dictionary annotations, + IIndex index, + RuntimeIndex runtimeIndex, + bool runtime) + { + base.ProcessIndexAnnotations(annotations, index, runtimeIndex, runtime); + + if (!runtime) + { + annotations.Remove(MySqlAnnotationNames.FullTextIndex); + annotations.Remove(MySqlAnnotationNames.FullTextParser); + annotations.Remove(MySqlAnnotationNames.IndexPrefixLength); + annotations.Remove(MySqlAnnotationNames.SpatialIndex); + } + } + + /// + protected override void ProcessKeyAnnotations( + Dictionary annotations, + IKey key, + RuntimeKey runtimeKey, + bool runtime) + { + base.ProcessKeyAnnotations(annotations, key, runtimeKey, runtime); + + if (!runtime) + { + annotations.Remove(MySqlAnnotationNames.IndexPrefixLength); + } + } +} diff --git a/src/EFCore.MySql/Metadata/Conventions/MySqlValueGenerationStrategyConvention.cs b/src/EFCore.MySql/Metadata/Conventions/MySqlValueGenerationStrategyConvention.cs new file mode 100644 index 000000000..b31e76ff0 --- /dev/null +++ b/src/EFCore.MySql/Metadata/Conventions/MySqlValueGenerationStrategyConvention.cs @@ -0,0 +1,109 @@ +// Copyright (c) Pomelo Foundation. All rights reserved. +// Licensed under the MIT. See LICENSE in the project root for license information. + +using System; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Metadata.Conventions; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; + +namespace Pomelo.EntityFrameworkCore.MySql.Metadata.Conventions; + +/// +/// A convention that configures the default model as +/// . +/// +public class MySqlValueGenerationStrategyConvention : IModelInitializedConvention, IModelFinalizingConvention +{ + /// + /// Creates a new instance of . + /// + /// Parameter object containing dependencies for this convention. + /// Parameter object containing relational dependencies for this convention. + public MySqlValueGenerationStrategyConvention( + ProviderConventionSetBuilderDependencies dependencies, + RelationalConventionSetBuilderDependencies relationalDependencies) + { + Dependencies = dependencies; + RelationalDependencies = relationalDependencies; + } + + /// + /// Parameter object containing service dependencies. + /// + protected virtual ProviderConventionSetBuilderDependencies Dependencies { get; } + + /// + /// Relational provider-specific dependencies for this service. + /// + protected virtual RelationalConventionSetBuilderDependencies RelationalDependencies { get; } + + /// + public virtual void ProcessModelInitialized(IConventionModelBuilder modelBuilder, IConventionContext context) + => modelBuilder.HasValueGenerationStrategy(MySqlValueGenerationStrategy.IdentityColumn); + + /// + public virtual void ProcessModelFinalizing( + IConventionModelBuilder modelBuilder, + IConventionContext context) + { + foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()) + { + foreach (var property in entityType.GetDeclaredProperties()) + { + MySqlValueGenerationStrategy? strategy = null; + var declaringTable = property.GetMappedStoreObjects(StoreObjectType.Table).FirstOrDefault(); + if (declaringTable.Name != null!) + { + strategy = property.GetValueGenerationStrategy(declaringTable, Dependencies.TypeMappingSource); + if (strategy == MySqlValueGenerationStrategy.None && + !IsStrategyNoneNeeded(property, declaringTable)) + { + strategy = null; + } + } + else + { + var declaringView = property.GetMappedStoreObjects(StoreObjectType.View).FirstOrDefault(); + if (declaringView.Name != null!) + { + strategy = property.GetValueGenerationStrategy(declaringView, Dependencies.TypeMappingSource); + if (strategy == MySqlValueGenerationStrategy.None && + !IsStrategyNoneNeeded(property, declaringView)) + { + strategy = null; + } + } + } + + // Needed for the annotation to show up in the model snapshot. + if (strategy != null && + declaringTable.Name != null) + { + property.Builder.HasValueGenerationStrategy(strategy); + } + } + } + + bool IsStrategyNoneNeeded(IReadOnlyProperty property, StoreObjectIdentifier storeObject) + { + if (property.ValueGenerated == ValueGenerated.OnAdd && + !property.TryGetDefaultValue(storeObject, out _) && + property.GetDefaultValueSql(storeObject) is null && + property.GetComputedColumnSql(storeObject) is null && + property.DeclaringType.Model.GetValueGenerationStrategy() != MySqlValueGenerationStrategy.None) + { + var providerClrType = (property.GetValueConverter() ?? + (property.FindRelationalTypeMapping(storeObject) ?? + Dependencies.TypeMappingSource.FindMapping((IProperty)property))?.Converter) + ?.ProviderClrType.UnwrapNullableType(); + + return providerClrType is not null && (providerClrType.IsInteger()); + } + + return false; + } + } +} diff --git a/src/EFCore.MySql/Migrations/MySqlMigrationsSqlGenerator.cs b/src/EFCore.MySql/Migrations/MySqlMigrationsSqlGenerator.cs index 90e351a09..390a17589 100644 --- a/src/EFCore.MySql/Migrations/MySqlMigrationsSqlGenerator.cs +++ b/src/EFCore.MySql/Migrations/MySqlMigrationsSqlGenerator.cs @@ -1131,8 +1131,8 @@ protected override void ColumnDefinition( $"Error in {table}.{name}: DATETIME does not support values generated " + $"on Add or Update in server version {_options.ServerVersion}. Try explicitly setting the column type to TIMESTAMP."); } - goto case "timestamp"; + case "timestamp": operation.DefaultValueSql = $"CURRENT_TIMESTAMP({matchLen})"; break; diff --git a/src/EFCore.MySql/Query/ExpressionVisitors/Internal/MySqlParameterInliningExpressionVisitor.cs b/src/EFCore.MySql/Query/ExpressionVisitors/Internal/MySqlParameterInliningExpressionVisitor.cs index b79ae84ad..2d3885cc5 100644 --- a/src/EFCore.MySql/Query/ExpressionVisitors/Internal/MySqlParameterInliningExpressionVisitor.cs +++ b/src/EFCore.MySql/Query/ExpressionVisitors/Internal/MySqlParameterInliningExpressionVisitor.cs @@ -1,6 +1,7 @@ // Copyright (c) Pomelo Foundation. All rights reserved. // Licensed under the MIT. See LICENSE in the project root for license information. +using System; using System.Collections.Generic; using System.Linq.Expressions; using Microsoft.EntityFrameworkCore.Query; @@ -25,7 +26,7 @@ public class MySqlParameterInliningExpressionVisitor : ExpressionVisitor private IReadOnlyDictionary _parametersValues; private bool _canCache; - private bool _inJsonTableSourceParameterCall; + private bool _shouldInlineParameters; public MySqlParameterInliningExpressionVisitor( IRelationalTypeMappingSource typeMappingSource, @@ -43,6 +44,7 @@ public virtual Expression Process(Expression expression, IReadOnlyDictionary extensionExpression switch { MySqlJsonTableExpression jsonTableExpression => VisitJsonTable(jsonTableExpression), + SelectExpression selectExpression => VisitSelect(selectExpression), SqlParameterExpression sqlParameterExpression => VisitSqlParameter(sqlParameterExpression), ShapedQueryExpression shapedQueryExpression => shapedQueryExpression.Update( Visit(shapedQueryExpression.QueryExpression), @@ -62,36 +65,65 @@ protected override Expression VisitExtension(Expression extensionExpression) _ => base.VisitExtension(extensionExpression) }; + protected virtual Expression VisitSelect(SelectExpression selectExpression) + => NewInlineParametersScope( + inlineParameters: false, + () => base.VisitExtension(selectExpression)); + // => NewInlineParametersScope( + // inlineParameters: false, + // () => selectExpression.Offset is not null + // ? selectExpression.Update( + // selectExpression.Projection, + // selectExpression.Tables, + // selectExpression.Predicate, + // selectExpression.GroupBy, + // selectExpression.Having, + // selectExpression.Orderings, + // selectExpression.Limit, + // NewInlineParametersScope( + // inlineParameters: true, + // () => (SqlExpression)Visit(selectExpression.Offset))) + // : base.VisitExtension(selectExpression)); + + // For test simplicity, we currently inline parameters even for non MySQL database engines (even though it should not be necessary + // for e.g. MariaDB). + // TODO: Use inlined parameters only if JsonTableImplementationUsingParameterAsSourceWithoutEngineCrash is true. protected virtual Expression VisitJsonTable(MySqlJsonTableExpression jsonTableExpression) - { - var parentInJsonTableSourceParameterCall = _inJsonTableSourceParameterCall; - _inJsonTableSourceParameterCall = true; - var jsonExpression = (SqlExpression)Visit(jsonTableExpression.JsonExpression); - _inJsonTableSourceParameterCall = parentInJsonTableSourceParameterCall; - - return jsonTableExpression.Update( - jsonExpression, + => jsonTableExpression.Update( + NewInlineParametersScope( + inlineParameters: true, + () => (SqlExpression)Visit(jsonTableExpression.JsonExpression)), jsonTableExpression.Path, jsonTableExpression.ColumnInfos); - } protected virtual Expression VisitSqlParameter(SqlParameterExpression sqlParameterExpression) { - // For test simplicity, we currently inline parameters even for non MySQL database engines (even though it should not be necessary - // for e.g. MariaDB). - // TODO: Use inlined parameters only if JsonTableImplementationUsingParameterAsSourceWithoutEngineCrash is true. - if (_inJsonTableSourceParameterCall /*&& - !_options.ServerVersion.Supports.JsonTableImplementationUsingParameterAsSourceWithoutEngineCrash*/) + if (!_shouldInlineParameters) { - _canCache = false; - - return new MySqlInlinedParameterExpression( - sqlParameterExpression, - _sqlExpressionFactory.Constant( - _parametersValues[sqlParameterExpression.Name], - sqlParameterExpression.TypeMapping)); + return sqlParameterExpression; } - return sqlParameterExpression; + _canCache = false; + + return new MySqlInlinedParameterExpression( + sqlParameterExpression, + _sqlExpressionFactory.Constant( + _parametersValues[sqlParameterExpression.Name], + sqlParameterExpression.TypeMapping)); + } + + protected virtual T NewInlineParametersScope(bool inlineParameters, Func func) + { + var parentShouldInlineParameters = _shouldInlineParameters; + _shouldInlineParameters = inlineParameters; + + try + { + return func(); + } + finally + { + _shouldInlineParameters = parentShouldInlineParameters; + } } } diff --git a/test/EFCore.MySql.FunctionalTests/BadDataJsonDeserializationMySqlTest.cs b/test/EFCore.MySql.FunctionalTests/BadDataJsonDeserializationMySqlTest.cs new file mode 100644 index 000000000..0e5ef5b42 --- /dev/null +++ b/test/EFCore.MySql.FunctionalTests/BadDataJsonDeserializationMySqlTest.cs @@ -0,0 +1,10 @@ +using Microsoft.EntityFrameworkCore; +using Pomelo.EntityFrameworkCore.MySql.Tests; + +namespace Pomelo.EntityFrameworkCore.MySql.FunctionalTests; + +public class BadDataJsonDeserializationMySqlTest : BadDataJsonDeserializationTestBase +{ + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => base.OnConfiguring(optionsBuilder.UseMySql(AppConfig.ServerVersion, b => b.UseNetTopologySuite())); +} diff --git a/test/EFCore.MySql.FunctionalTests/Query/PrimitiveCollectionsQueryMySqlTest.cs b/test/EFCore.MySql.FunctionalTests/Query/PrimitiveCollectionsQueryMySqlTest.cs index 41a70ca09..ccfec4814 100644 --- a/test/EFCore.MySql.FunctionalTests/Query/PrimitiveCollectionsQueryMySqlTest.cs +++ b/test/EFCore.MySql.FunctionalTests/Query/PrimitiveCollectionsQueryMySqlTest.cs @@ -1658,6 +1658,36 @@ public override async Task Column_collection_Concat_parameter_collection_equalit AssertSql(); } + public override async Task Nested_contains_with_Lists_and_no_inferred_type_mapping(bool async) + { + await base.Nested_contains_with_Lists_and_no_inferred_type_mapping(async); + + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE CASE + WHEN `p`.`Int` IN (1, 2, 3) THEN 'one' + ELSE 'two' +END IN ('one', 'two', 'three') +"""); + } + + public override async Task Nested_contains_with_arrays_and_no_inferred_type_mapping(bool async) + { + await base.Nested_contains_with_arrays_and_no_inferred_type_mapping(async); + + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE CASE + WHEN `p`.`Int` IN (1, 2, 3) THEN 'one' + ELSE 'two' +END IN ('one', 'two', 'three') +"""); + } + [ConditionalFact] public virtual void Check_all_tests_overridden() => TestHelpers.AssertAllMethodsOverridden(GetType()); diff --git a/test/EFCore.MySql.FunctionalTests/Query/TPCGearsOfWarQueryMySqlTest.cs b/test/EFCore.MySql.FunctionalTests/Query/TPCGearsOfWarQueryMySqlTest.cs index 125fc00bb..878e31a31 100644 --- a/test/EFCore.MySql.FunctionalTests/Query/TPCGearsOfWarQueryMySqlTest.cs +++ b/test/EFCore.MySql.FunctionalTests/Query/TPCGearsOfWarQueryMySqlTest.cs @@ -13532,6 +13532,88 @@ SELECT 1 """); } + public override async Task Nav_expansion_inside_Contains_argument(bool async) + { + await base.Nav_expansion_inside_Contains_argument(async); + + AssertSql( +""" +SELECT `t`.`Nickname`, `t`.`SquadId`, `t`.`AssignedCityName`, `t`.`CityOfBirthName`, `t`.`FullName`, `t`.`HasSoulPatch`, `t`.`LeaderNickname`, `t`.`LeaderSquadId`, `t`.`Rank`, `t`.`Discriminator` +FROM ( + SELECT `g`.`Nickname`, `g`.`SquadId`, `g`.`AssignedCityName`, `g`.`CityOfBirthName`, `g`.`FullName`, `g`.`HasSoulPatch`, `g`.`LeaderNickname`, `g`.`LeaderSquadId`, `g`.`Rank`, 'Gear' AS `Discriminator` + FROM `Gears` AS `g` + UNION ALL + SELECT `o`.`Nickname`, `o`.`SquadId`, `o`.`AssignedCityName`, `o`.`CityOfBirthName`, `o`.`FullName`, `o`.`HasSoulPatch`, `o`.`LeaderNickname`, `o`.`LeaderSquadId`, `o`.`Rank`, 'Officer' AS `Discriminator` + FROM `Officers` AS `o` +) AS `t` +WHERE CASE + WHEN EXISTS ( + SELECT 1 + FROM `Weapons` AS `w` + WHERE `t`.`FullName` = `w`.`OwnerFullName`) THEN 1 + ELSE 0 +END IN (1, -1) +"""); + } + + public override async Task Nav_expansion_with_member_pushdown_inside_Contains_argument(bool async) + { + await base.Nav_expansion_with_member_pushdown_inside_Contains_argument(async); + + AssertSql( +""" +SELECT `t`.`Nickname`, `t`.`SquadId`, `t`.`AssignedCityName`, `t`.`CityOfBirthName`, `t`.`FullName`, `t`.`HasSoulPatch`, `t`.`LeaderNickname`, `t`.`LeaderSquadId`, `t`.`Rank`, `t`.`Discriminator` +FROM ( + SELECT `g`.`Nickname`, `g`.`SquadId`, `g`.`AssignedCityName`, `g`.`CityOfBirthName`, `g`.`FullName`, `g`.`HasSoulPatch`, `g`.`LeaderNickname`, `g`.`LeaderSquadId`, `g`.`Rank`, 'Gear' AS `Discriminator` + FROM `Gears` AS `g` + UNION ALL + SELECT `o`.`Nickname`, `o`.`SquadId`, `o`.`AssignedCityName`, `o`.`CityOfBirthName`, `o`.`FullName`, `o`.`HasSoulPatch`, `o`.`LeaderNickname`, `o`.`LeaderSquadId`, `o`.`Rank`, 'Officer' AS `Discriminator` + FROM `Officers` AS `o` +) AS `t` +WHERE ( + SELECT `w`.`Name` + FROM `Weapons` AS `w` + WHERE `t`.`FullName` = `w`.`OwnerFullName` + ORDER BY `w`.`Id` + LIMIT 1) IN ('Marcus'' Lancer', 'Dom''s Gnasher') +"""); + } + + public override async Task Subquery_inside_Take_argument(bool async) + { + await base.Subquery_inside_Take_argument(async); + + AssertSql(""); + } + + public override async Task Nav_expansion_inside_Skip_correlated_to_source(bool async) + { + await base.Nav_expansion_inside_Skip_correlated_to_source(async); + + AssertSql(""); + } + + public override async Task Nav_expansion_inside_Take_correlated_to_source(bool async) + { + await base.Nav_expansion_inside_Take_correlated_to_source(async); + + AssertSql(""); + } + + public override async Task Nav_expansion_with_member_pushdown_inside_Take_correlated_to_source(bool async) + { + await base.Nav_expansion_with_member_pushdown_inside_Take_correlated_to_source(async); + + AssertSql(""); + } + + public override async Task Nav_expansion_inside_ElementAt_correlated_to_source(bool async) + { + await base.Nav_expansion_inside_ElementAt_correlated_to_source(async); + + AssertSql(""); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } diff --git a/test/EFCore.MySql.FunctionalTests/TestUtilities/MySqlNorthwindTestStoreFactory.cs b/test/EFCore.MySql.FunctionalTests/TestUtilities/MySqlNorthwindTestStoreFactory.cs index f2511c0fe..f35a8a6ae 100644 --- a/test/EFCore.MySql.FunctionalTests/TestUtilities/MySqlNorthwindTestStoreFactory.cs +++ b/test/EFCore.MySql.FunctionalTests/TestUtilities/MySqlNorthwindTestStoreFactory.cs @@ -5,11 +5,11 @@ namespace Pomelo.EntityFrameworkCore.MySql.FunctionalTests.TestUtilities { public class MySqlNorthwindTestStoreFactory : MySqlTestStoreFactory { - public const string DefaultName = "Northwind"; + public const string DefaultNamePrefix = "Northwind"; public static new MySqlNorthwindTestStoreFactory Instance => InstanceCi; - public static MySqlNorthwindTestStoreFactory InstanceCi { get; } = new MySqlNorthwindTestStoreFactory(databaseCollation: AppConfig.ServerVersion.DefaultUtf8CiCollation); - public static MySqlNorthwindTestStoreFactory InstanceCs { get; } = new MySqlNorthwindTestStoreFactory(databaseCollation: AppConfig.ServerVersion.DefaultUtf8CsCollation); + public static new MySqlNorthwindTestStoreFactory InstanceCi { get; } = new MySqlNorthwindTestStoreFactory(databaseCollation: AppConfig.ServerVersion.DefaultUtf8CiCollation); + public static new MySqlNorthwindTestStoreFactory InstanceCs { get; } = new MySqlNorthwindTestStoreFactory(databaseCollation: AppConfig.ServerVersion.DefaultUtf8CsCollation); public static new MySqlNorthwindTestStoreFactory NoBackslashEscapesInstance { get; } = new MySqlNorthwindTestStoreFactory(true); protected MySqlNorthwindTestStoreFactory(bool noBackslashEscapes = false, string databaseCollation = null) @@ -18,6 +18,6 @@ protected MySqlNorthwindTestStoreFactory(bool noBackslashEscapes = false, string } public override TestStore GetOrCreate(string storeName) - => MySqlTestStore.GetOrCreate(storeName ?? DefaultName, "Northwind.sql", noBackslashEscapes: NoBackslashEscapes, databaseCollation: DatabaseCollation); + => MySqlTestStore.GetOrCreate(storeName ?? $"{DefaultNamePrefix}__{DatabaseCollation}", "Northwind.sql", noBackslashEscapes: NoBackslashEscapes, databaseCollation: DatabaseCollation); } } diff --git a/test/EFCore.MySql.FunctionalTests/TestUtilities/MySqlTestStoreFactory.cs b/test/EFCore.MySql.FunctionalTests/TestUtilities/MySqlTestStoreFactory.cs index c64ae1320..a3d18583f 100644 --- a/test/EFCore.MySql.FunctionalTests/TestUtilities/MySqlTestStoreFactory.cs +++ b/test/EFCore.MySql.FunctionalTests/TestUtilities/MySqlTestStoreFactory.cs @@ -1,12 +1,16 @@ using Microsoft.EntityFrameworkCore.TestUtilities; using Microsoft.Extensions.DependencyInjection; using MySqlConnector; +using Pomelo.EntityFrameworkCore.MySql.Tests; namespace Pomelo.EntityFrameworkCore.MySql.FunctionalTests.TestUtilities { public class MySqlTestStoreFactory : RelationalTestStoreFactory { - public static MySqlTestStoreFactory Instance { get; } = new MySqlTestStoreFactory(); + public static MySqlTestStoreFactory Instance => InstanceCi; + public static MySqlTestStoreFactory InstanceCi { get; } = new MySqlTestStoreFactory(databaseCollation: AppConfig.ServerVersion.DefaultUtf8CiCollation); + public static MySqlTestStoreFactory InstanceCs { get; } = new MySqlTestStoreFactory(databaseCollation: AppConfig.ServerVersion.DefaultUtf8CsCollation); + public static MySqlTestStoreFactory NoBackslashEscapesInstance { get; } = new MySqlTestStoreFactory(noBackslashEscapes: true); public static MySqlTestStoreFactory GuidBinary16Instance { get; } = new MySqlTestStoreFactory(guidFormat: MySqlGuidFormat.Binary16); diff --git a/test/EFCore.MySql.FunctionalTests/TransactionMySqlTest.cs b/test/EFCore.MySql.FunctionalTests/TransactionMySqlTest.cs index 921a37592..165f5a0b6 100644 --- a/test/EFCore.MySql.FunctionalTests/TransactionMySqlTest.cs +++ b/test/EFCore.MySql.FunctionalTests/TransactionMySqlTest.cs @@ -15,9 +15,8 @@ public TransactionMySqlTest(TransactionMySqlFixture fixture) { } - protected override bool SnapshotSupported => false; + protected override bool SnapshotSupported => true; protected override bool AmbientTransactionsSupported => true; - protected override bool DirtyReadsOccur => false; protected override DbContext CreateContextWithConnectionString() {