diff --git a/Directory.Packages.props b/Directory.Packages.props index 5975e2b9a33..662be990703 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -36,7 +36,7 @@ - + diff --git a/NuGet.config b/NuGet.config index e6046bdda5d..d6eecbfcd48 100644 --- a/NuGet.config +++ b/NuGet.config @@ -4,7 +4,7 @@ - + @@ -21,7 +21,7 @@ - + diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 382f139f1d0..78837f93284 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -170,36 +170,11 @@ extends: - _runCounter: $[counter(variables['Build.Reason'], 0)] # Rely on task Arcade injects, not auto-injected build step. - skipComponentGovernanceDetection: true - - ${{ if notin(variables['Build.Reason'], 'PullRequest', 'Schedule') }}: - - _CosmosConnectionUrl: 'true' steps: - - bash: | - echo "##vso[task.setvariable variable=_CosmosConnectionUrl]https://ef-nightly-test.documents.azure.com:443/" - displayName: Prepare to run Cosmos tests on ef-nightly-test - condition: and(eq(variables['_CosmosConnectionUrl'], 'true'), or(endsWith(variables['_runCounter'], '0'), endsWith(variables['_runCounter'], '2'), endsWith(variables['_runCounter'], '4'), endsWith(variables['_runCounter'], '6'), endsWith(variables['_runCounter'], '8'))) - - bash: | - echo "##vso[task.setvariable variable=_CosmosConnectionUrl]https://ef-pr-test.documents.azure.com:443/" - displayName: Prepare to run Cosmos tests on ef-pr-test - condition: and(eq(variables['_CosmosConnectionUrl'], 'true'), or(endsWith(variables['_runCounter'], '1'), endsWith(variables['_runCounter'], '3'), endsWith(variables['_runCounter'], '5'), endsWith(variables['_runCounter'], '7'), endsWith(variables['_runCounter'], '9'))) - template: /eng/common/templates-official/steps/enable-internal-sources.yml - template: /eng/common/templates-official/steps/enable-internal-runtimes.yml - script: eng/common/cibuild.sh --configuration $(_BuildConfig) --prepareMachine $(_InternalRuntimeDownloadArgs) displayName: Build - - task: AzureCLI@2 - displayName: Run Cosmos tests - condition: notin(variables['Build.Reason'], 'PullRequest', 'Schedule') - inputs: - azureSubscription: EFCosmosTesting - scriptType: bash - scriptLocation: 'inlineScript' - inlineScript: | - ./test.sh --ci --configuration $(_BuildConfig) --projects $(Build.SourcesDirectory)/test/EFCore.Cosmos.FunctionalTests/EFCore.Cosmos.FunctionalTests.csproj - env: - Test__Cosmos__DefaultConnection: $(_CosmosConnectionUrl) - Test__Cosmos__UseTokenCredential: true - Test__Cosmos__SubscriptionId: d709b837-4a74-4aec-addc-b6e4b9b23e7e - Test__Cosmos__ResourceGroup: efcosmosci - name: Build templateContext: outputs: - output: pipelineArtifact diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 1dc45f4c790..47284fc7f22 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,83 +1,83 @@ - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 - + https://github.com/dotnet/arcade - f33d9e642f0e68a61312164cd9e0baf4e142a999 + aa61e8c20a869bcc994f8b29eb07d927d2bec6f4 - + https://github.com/dotnet/arcade - f33d9e642f0e68a61312164cd9e0baf4e142a999 + aa61e8c20a869bcc994f8b29eb07d927d2bec6f4 - + https://github.com/dotnet/arcade - f33d9e642f0e68a61312164cd9e0baf4e142a999 + aa61e8c20a869bcc994f8b29eb07d927d2bec6f4 diff --git a/eng/Versions.props b/eng/Versions.props index a9458f9340d..9fe166c9403 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -1,6 +1,6 @@ - 9.0.4 + 9.0.5 rtm @@ -17,24 +17,24 @@ False - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4-servicing.25163.5 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4-servicing.25163.5 - 9.0.4 - 9.0.4 - 9.0.4 + 9.0.5 + 9.0.5 + 9.0.5 + 9.0.5 + 9.0.5 + 9.0.5 + 9.0.5 + 9.0.5-servicing.25215.9 + 9.0.5 + 9.0.5 + 9.0.5 + 9.0.5-servicing.25215.9 + 9.0.5 + 9.0.5 + 9.0.5 - 9.0.0-beta.25161.4 + 9.0.0-beta.25208.6 17.8.3 diff --git a/eng/common/tools.ps1 b/eng/common/tools.ps1 index a46b6deb759..22b49e09d09 100644 --- a/eng/common/tools.ps1 +++ b/eng/common/tools.ps1 @@ -42,7 +42,7 @@ [bool]$useInstalledDotNetCli = if (Test-Path variable:useInstalledDotNetCli) { $useInstalledDotNetCli } else { $true } # Enable repos to use a particular version of the on-line dotnet-install scripts. -# default URL: https://dotnet.microsoft.com/download/dotnet/scripts/v1/dotnet-install.ps1 +# default URL: https://builds.dotnet.microsoft.com/dotnet/scripts/v1/dotnet-install.ps1 [string]$dotnetInstallScriptVersion = if (Test-Path variable:dotnetInstallScriptVersion) { $dotnetInstallScriptVersion } else { 'v1' } # True to use global NuGet cache instead of restoring packages to repository-local directory. @@ -262,7 +262,7 @@ function GetDotNetInstallScript([string] $dotnetRoot) { if (!(Test-Path $installScript)) { Create-Directory $dotnetRoot $ProgressPreference = 'SilentlyContinue' # Don't display the console progress UI - it's a huge perf hit - $uri = "https://dotnet.microsoft.com/download/dotnet/scripts/$dotnetInstallScriptVersion/dotnet-install.ps1" + $uri = "https://builds.dotnet.microsoft.com/dotnet/scripts/$dotnetInstallScriptVersion/dotnet-install.ps1" Retry({ Write-Host "GET $uri" diff --git a/eng/common/tools.sh b/eng/common/tools.sh index 1159726a10f..01b09b65796 100755 --- a/eng/common/tools.sh +++ b/eng/common/tools.sh @@ -54,7 +54,7 @@ warn_as_error=${warn_as_error:-true} use_installed_dotnet_cli=${use_installed_dotnet_cli:-true} # Enable repos to use a particular version of the on-line dotnet-install scripts. -# default URL: https://dotnet.microsoft.com/download/dotnet/scripts/v1/dotnet-install.sh +# default URL: https://builds.dotnet.microsoft.com/dotnet/scripts/v1/dotnet-install.sh dotnetInstallScriptVersion=${dotnetInstallScriptVersion:-'v1'} # True to use global NuGet cache instead of restoring packages to repository-local directory. @@ -295,7 +295,7 @@ function with_retries { function GetDotNetInstallScript { local root=$1 local install_script="$root/dotnet-install.sh" - local install_script_url="https://dotnet.microsoft.com/download/dotnet/scripts/$dotnetInstallScriptVersion/dotnet-install.sh" + local install_script_url="https://builds.dotnet.microsoft.com/dotnet/scripts/$dotnetInstallScriptVersion/dotnet-install.sh" if [[ ! -a "$install_script" ]]; then mkdir -p "$root" diff --git a/global.json b/global.json index 61faa20a7a5..70395de4fa1 100644 --- a/global.json +++ b/global.json @@ -1,11 +1,11 @@ { "sdk": { - "version": "9.0.104", + "version": "9.0.105", "allowPrerelease": true, "rollForward": "latestMajor" }, "tools": { - "dotnet": "9.0.104", + "dotnet": "9.0.105", "runtimes": { "dotnet": [ "$(MicrosoftNETCoreBrowserDebugHostTransportVersion)" @@ -13,7 +13,7 @@ } }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "9.0.0-beta.25161.4", - "Microsoft.DotNet.Helix.Sdk": "9.0.0-beta.25161.4" + "Microsoft.DotNet.Arcade.Sdk": "9.0.0-beta.25208.6", + "Microsoft.DotNet.Helix.Sdk": "9.0.0-beta.25208.6" } } diff --git a/src/EFCore.Analyzers/EFDiagnostics.cs b/src/EFCore.Analyzers/EFDiagnostics.cs index c3021424089..13d885f600c 100644 --- a/src/EFCore.Analyzers/EFDiagnostics.cs +++ b/src/EFCore.Analyzers/EFDiagnostics.cs @@ -19,4 +19,5 @@ public static class EFDiagnostics public const string MetricsExperimental = "EF9101"; public const string PagingExperimental = "EF9102"; public const string CosmosVectorSearchExperimental = "EF9103"; + public const string CosmosFullTextSearchExperimental = "EF9104"; } diff --git a/src/EFCore.Cosmos/EFCore.Cosmos.csproj b/src/EFCore.Cosmos/EFCore.Cosmos.csproj index e5189d571d3..a3bd6fe8baf 100644 --- a/src/EFCore.Cosmos/EFCore.Cosmos.csproj +++ b/src/EFCore.Cosmos/EFCore.Cosmos.csproj @@ -12,6 +12,7 @@ $(NoWarn);EF9101 $(NoWarn);EF9102 $(NoWarn);EF9103 + $(NoWarn);EF9104 @@ -49,6 +50,8 @@ + + diff --git a/src/EFCore.Cosmos/Extensions/CosmosDbFunctionsExtensions.cs b/src/EFCore.Cosmos/Extensions/CosmosDbFunctionsExtensions.cs index 3dc681450be..c34b68cd07b 100644 --- a/src/EFCore.Cosmos/Extensions/CosmosDbFunctionsExtensions.cs +++ b/src/EFCore.Cosmos/Extensions/CosmosDbFunctionsExtensions.cs @@ -52,6 +52,60 @@ public static T CoalesceUndefined( T expression2) => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(CoalesceUndefined))); + /// + /// Checks if the specified property contains the given keyword using full-text search. + /// + /// The instance. + /// The property to search. + /// The keyword to search for. + /// if the property contains the keyword; otherwise, . + [Experimental(EFDiagnostics.CosmosFullTextSearchExperimental)] + public static bool FullTextContains(this DbFunctions _, string property, string keyword) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(FullTextContains))); + + /// + /// Checks if the specified property contains all the given keywords using full-text search. + /// + /// The instance. + /// The property to search. + /// The keywords to search for. + /// if the property contains all the keywords; otherwise, . + [Experimental(EFDiagnostics.CosmosFullTextSearchExperimental)] + public static bool FullTextContainsAll(this DbFunctions _, string property, params string[] keywords) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(FullTextContainsAll))); + + /// + /// Checks if the specified property contains any of the given keywords using full-text search. + /// + /// The instance. + /// The property to search. + /// The keywords to search for. + /// if the property contains any of the keywords; otherwise, . + [Experimental(EFDiagnostics.CosmosFullTextSearchExperimental)] + public static bool FullTextContainsAny(this DbFunctions _, string property, params string[] keywords) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(FullTextContainsAny))); + + /// + /// Returns the full-text search score for the specified property and keywords. + /// + /// The instance. + /// The property to score. + /// The keywords to score by. + /// The full-text search score. + [Experimental(EFDiagnostics.CosmosFullTextSearchExperimental)] + public static double FullTextScore(this DbFunctions _, string property, params string[] keywords) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(FullTextScore))); + + /// + /// Combines scores provided by two or more specified functions. + /// + /// The instance. + /// The functions to compute the score for. + /// The combined score. + [Experimental(EFDiagnostics.CosmosFullTextSearchExperimental)] + public static double Rrf(this DbFunctions _, params double[] functions) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Rrf))); + /// /// Returns the distance between two vectors, using the distance function and data type defined using /// + /// Ordering based on scoring function is not supported inside '{orderByDescending}'. Use '{orderBy}' instead. + /// + public static string OrderByDescendingScoringFunction(object? orderByDescending, object? orderBy) + => string.Format( + GetString("OrderByDescendingScoringFunction", nameof(orderByDescending), nameof(orderBy)), + orderByDescending, orderBy); + + /// + /// Only one ordering using scoring function is allowed. Use 'EF.Functions.{rrf}' method to combine multiple scoring functions. + /// + public static string OrderByMultipleScoringFunctionWithoutRrf(object? rrf) + => string.Format( + GetString("OrderByMultipleScoringFunctionWithoutRrf", nameof(rrf)), + rrf); + + /// + /// Ordering using a scoring function is mutually exclusive with other forms of ordering. + /// + public static string OrderByScoringFunctionMixedWithRegularOrderby + => GetString("OrderByScoringFunctionMixedWithRegularOrderby"); + /// /// The entity of type '{entityType}' is mapped as a part of the document mapped to '{missingEntityType}', but there is no tracked entity of this type with the corresponding key value. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the key values. /// diff --git a/src/EFCore.Cosmos/Properties/CosmosStrings.resx b/src/EFCore.Cosmos/Properties/CosmosStrings.resx index 8f9a875524b..09182e651e6 100644 --- a/src/EFCore.Cosmos/Properties/CosmosStrings.resx +++ b/src/EFCore.Cosmos/Properties/CosmosStrings.resx @@ -283,6 +283,15 @@ Exactly one of '{param1}' or '{param2}' must be set. + + Ordering based on scoring function is not supported inside '{orderByDescending}'. Use '{orderBy}' instead. + + + Only one ordering using scoring function is allowed. Use 'EF.Functions.{rrf}' method to combine multiple scoring functions. + + + Ordering using a scoring function is mutually exclusive with other forms of ordering. + The entity of type '{entityType}' is mapped as a part of the document mapped to '{missingEntityType}', but there is no tracked entity of this type with the corresponding key value. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the key values. diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosMethodCallTranslatorProvider.cs b/src/EFCore.Cosmos/Query/Internal/CosmosMethodCallTranslatorProvider.cs index 352e3d443e1..000c449a7c0 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosMethodCallTranslatorProvider.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosMethodCallTranslatorProvider.cs @@ -36,7 +36,8 @@ public CosmosMethodCallTranslatorProvider( new CosmosRegexTranslator(sqlExpressionFactory), new CosmosStringMethodTranslator(sqlExpressionFactory), new CosmosTypeCheckingTranslator(sqlExpressionFactory), - new CosmosVectorSearchTranslator(sqlExpressionFactory, typeMappingSource) + new CosmosVectorSearchTranslator(sqlExpressionFactory, typeMappingSource), + new CosmosFullTextSearchTranslator(sqlExpressionFactory, typeMappingSource) //new LikeTranslator(sqlExpressionFactory), //new EnumHasFlagTranslator(sqlExpressionFactory), //new GetValueOrDefaultTranslator(sqlExpressionFactory), diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs index 67b1437208a..5da126d0827 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs @@ -14,6 +14,9 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; /// public class CosmosQuerySqlGenerator(ITypeMappingSource typeMappingSource) : SqlExpressionVisitor { + private static readonly bool UseOldBehavior35476 = + AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue35476", out var enabled35476) && enabled35476; + private readonly IndentedStringBuilder _sqlBuilder = new(); private IReadOnlyDictionary _parameterValues = null!; private List _sqlParameters = null!; @@ -341,6 +344,15 @@ protected override Expression VisitSelect(SelectExpression selectExpression) { _sqlBuilder.AppendLine().Append("ORDER BY "); + var orderByScoringFunction = selectExpression.Orderings is [{ Expression: SqlFunctionExpression { IsScoringFunction: true } }]; + if (!UseOldBehavior35476 && orderByScoringFunction) + { + _sqlBuilder.Append("RANK "); + } + + Check.DebugAssert(UseOldBehavior35476 || orderByScoringFunction || selectExpression.Orderings.All(x => x.Expression is not SqlFunctionExpression { IsScoringFunction: true }), + "Scoring function can only appear as first (and only) ordering, or not at all."); + GenerateList(selectExpression.Orderings, e => Visit(e)); } diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs index d349d73e609..63553c5eba8 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs @@ -20,6 +20,9 @@ private abstract class CosmosProjectionBindingRemovingExpressionVisitorBase( bool trackQueryResults) : ExpressionVisitor { + private static readonly bool UseOldBehavior21006 = + AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue21006", out var enabled21006) && enabled21006; + private static readonly MethodInfo GetItemMethodInfo = typeof(JObject).GetRuntimeProperties() .Single(pi => pi.Name == "Item" && pi.GetIndexParameters()[0].ParameterType == typeof(string)) @@ -691,7 +694,11 @@ private Expression CreateGetValueExpression( && !property.IsShadowProperty()) { var readExpression = CreateGetValueExpression( - jTokenExpression, storeName, type.MakeNullable(), property.GetTypeMapping()); + jTokenExpression, + storeName, + type.MakeNullable(), + property.GetTypeMapping(), + isNonNullableScalar: false); var nonNullReadExpression = readExpression; if (nonNullReadExpression.Type != type) @@ -712,7 +719,14 @@ private Expression CreateGetValueExpression( } return Convert( - CreateGetValueExpression(jTokenExpression, storeName, type.MakeNullable(), property.GetTypeMapping()), + CreateGetValueExpression( + jTokenExpression, + storeName, + type.MakeNullable(), + property.GetTypeMapping(), + // special case keys - we check them for null to see if the entity needs to be materialized, so we want to keep the null, rather than non-nullable default + // returning defaults is supposed to help with evolving the schema - so this doesn't concern keys anyway (they shouldn't evolve) + isNonNullableScalar: !property.IsNullable && !property.IsKey()), type); } @@ -720,7 +734,8 @@ private Expression CreateGetValueExpression( Expression jTokenExpression, string storeName, Type type, - CoreTypeMapping typeMapping = null) + CoreTypeMapping typeMapping = null, + bool isNonNullableScalar = false) { Check.DebugAssert(type.IsNullableType(), "Must read nullable type from JObject."); @@ -763,6 +778,7 @@ var body Constant(CosmosClientWrapper.Serializer)), converter.ConvertFromProviderExpression.Body); + var originalBodyType = body.Type; if (body.Type != type) { body = Convert(body, type); @@ -783,7 +799,11 @@ var body } else { - replaceExpression = Default(type); + replaceExpression = isNonNullableScalar && !UseOldBehavior21006 + ? Expression.Convert( + Default(originalBodyType), + type) + : Default(type); } body = Condition( @@ -799,7 +819,11 @@ var body } else { - valueExpression = ConvertJTokenToType(jTokenExpression, typeMapping?.ClrType.MakeNullable() ?? type); + valueExpression = ConvertJTokenToType( + jTokenExpression, + (isNonNullableScalar && !UseOldBehavior21006 + ? typeMapping?.ClrType + : typeMapping?.ClrType.MakeNullable()) ?? type); if (valueExpression.Type != type) { diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.InExpressionValuesExpandingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.InExpressionValuesExpandingExpressionVisitor.cs index 98563fd0a2a..e23cb211fe9 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.InExpressionValuesExpandingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.InExpressionValuesExpandingExpressionVisitor.cs @@ -9,48 +9,129 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; public partial class CosmosShapedQueryCompilingExpressionVisitor { - private sealed class InExpressionValuesExpandingExpressionVisitor( + private static readonly bool UseOldBehavior35476 = + AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue35476", out var enabled35476) && enabled35476; + + private sealed class ParameterInliner( ISqlExpressionFactory sqlExpressionFactory, IReadOnlyDictionary parametersValues) : ExpressionVisitor { protected override Expression VisitExtension(Expression expression) { - if (expression is InExpression inExpression) + if (!UseOldBehavior35476) { - IReadOnlyList values; + expression = base.VisitExtension(expression); + } - switch (inExpression) + switch (expression) + { + // Inlines array parameter of InExpression, transforming: 'item IN (@valuesArray)' to: 'item IN (value1, value2)' + case InExpression inExpression: { - case { Values: IReadOnlyList values2 }: - values = values2; - break; - - // TODO: IN with subquery (return immediately, nothing to do here) + IReadOnlyList values; - case { ValuesParameter: SqlParameterExpression valuesParameter }: + switch (inExpression) { - var typeMapping = valuesParameter.TypeMapping; - var mutableValues = new List(); - foreach (var value in (IEnumerable)parametersValues[valuesParameter.Name]) + case { Values: IReadOnlyList values2 }: + values = values2; + break; + + // TODO: IN with subquery (return immediately, nothing to do here) + + case { ValuesParameter: SqlParameterExpression valuesParameter }: { - mutableValues.Add(sqlExpressionFactory.Constant(value, value?.GetType() ?? typeof(object), typeMapping)); + var typeMapping = valuesParameter.TypeMapping; + var mutableValues = new List(); + foreach (var value in (IEnumerable)parametersValues[valuesParameter.Name]) + { + mutableValues.Add(sqlExpressionFactory.Constant(value, value?.GetType() ?? typeof(object), typeMapping)); + } + + values = mutableValues; + break; } - values = mutableValues; - break; + default: + throw new UnreachableException(); } - default: - throw new UnreachableException(); + return values.Count == 0 + ? sqlExpressionFactory.ApplyDefaultTypeMapping(sqlExpressionFactory.Constant(false)) + : sqlExpressionFactory.In((SqlExpression)Visit(inExpression.Item), values); } - return values.Count == 0 - ? sqlExpressionFactory.ApplyDefaultTypeMapping(sqlExpressionFactory.Constant(false)) - : sqlExpressionFactory.In((SqlExpression)Visit(inExpression.Item), values); - } + // Converts Offset and Limit parameters to constants when ORDER BY RANK is detected in the SelectExpression (i.e. we order by scoring function) + // Cosmos only supports constants in Offset and Limit for this scenario currently (ORDER BY RANK limitation) + case SelectExpression { Orderings: [{ Expression: SqlFunctionExpression { IsScoringFunction: true } }], Limit: var limit, Offset: var offset } hybridSearch + when !UseOldBehavior35476 && (limit is SqlParameterExpression || offset is SqlParameterExpression): + { + if (hybridSearch.Limit is SqlParameterExpression limitPrm) + { + hybridSearch.ApplyLimit( + sqlExpressionFactory.Constant( + parametersValues[limitPrm.Name], + limitPrm.TypeMapping)); + } + + if (hybridSearch.Offset is SqlParameterExpression offsetPrm) + { + hybridSearch.ApplyOffset( + sqlExpressionFactory.Constant( + parametersValues[offsetPrm.Name], + offsetPrm.TypeMapping)); + } + + return base.VisitExtension(expression); + } - return base.VisitExtension(expression); + // Inlines array parameter of full-text functions, transforming FullTextContainsAll(x, @keywordsArray) to FullTextContainsAll(x, keyword1, keyword2)) + case SqlFunctionExpression + { + Name: "FullTextContainsAny" or "FullTextContainsAll", + Arguments: [var property, SqlParameterExpression { TypeMapping: { ElementTypeMapping: var elementTypeMapping }, Type: Type type } keywords] + } fullTextContainsAllAnyFunction + when !UseOldBehavior35476 && type == typeof(string[]): + { + var keywordValues = new List(); + foreach (var value in (IEnumerable)parametersValues[keywords.Name]) + { + keywordValues.Add(sqlExpressionFactory.Constant(value, typeof(string), elementTypeMapping)); + } + + return sqlExpressionFactory.Function( + fullTextContainsAllAnyFunction.Name, + [property, .. keywordValues], + fullTextContainsAllAnyFunction.Type, + fullTextContainsAllAnyFunction.TypeMapping); + } + + // Inlines array parameter of full-text score, transforming FullTextScore(x, @keywordsArray) to FullTextScore(x, [keyword1, keyword2])) + case SqlFunctionExpression + { + Name: "FullTextScore", + IsScoringFunction: true, + Arguments: [var property, SqlParameterExpression { TypeMapping: { ElementTypeMapping: not null } typeMapping } keywords] + } fullTextScoreFunction + when !UseOldBehavior35476: + { + var keywordValues = new List(); + foreach (var value in (IEnumerable)parametersValues[keywords.Name]) + { + keywordValues.Add((string)value); + } + + return new SqlFunctionExpression( + fullTextScoreFunction.Name, + isScoringFunction: true, + [property, sqlExpressionFactory.Constant(keywordValues, typeMapping)], + fullTextScoreFunction.Type, + fullTextScoreFunction.TypeMapping); + } + + default: + return expression; + } } } } diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.PagingQueryingEnumerable.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.PagingQueryingEnumerable.cs index e90c24664a5..6e84ffa7827 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.PagingQueryingEnumerable.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.PagingQueryingEnumerable.cs @@ -75,7 +75,7 @@ public IAsyncEnumerator> GetAsyncEnumerator(CancellationToken canc private CosmosSqlQuery GenerateQuery() => _querySqlGeneratorFactory.Create().GetSqlQuery( - (SelectExpression)new InExpressionValuesExpandingExpressionVisitor( + (SelectExpression)new ParameterInliner( _sqlExpressionFactory, _cosmosQueryContext.ParameterValues) .Visit(_selectExpression), diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs index 07b0c22115c..26c7d885cdc 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs @@ -71,7 +71,7 @@ IEnumerator IEnumerable.GetEnumerator() private CosmosSqlQuery GenerateQuery() => _querySqlGeneratorFactory.Create().GetSqlQuery( - (SelectExpression)new InExpressionValuesExpandingExpressionVisitor( + (SelectExpression)new ParameterInliner( _sqlExpressionFactory, _cosmosQueryContext.ParameterValues) .Visit(_selectExpression), diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/FragmentExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/FragmentExpression.cs index cdb0bbff323..3efd431ca2f 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/FragmentExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/FragmentExpression.cs @@ -23,6 +23,14 @@ public class FragmentExpression(string fragment) : Expression, IPrintableExpress /// public virtual string Fragment { get; } = fragment; + /// + public override ExpressionType NodeType + => base.NodeType; + + /// + public override Type Type + => typeof(object); + /// protected override Expression VisitChildren(ExpressionVisitor visitor) => this; diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs index 3f31abdf5a8..5b735680b43 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.EntityFrameworkCore.Cosmos.Extensions; using Microsoft.EntityFrameworkCore.Cosmos.Internal; using Microsoft.EntityFrameworkCore.Internal; @@ -16,6 +17,9 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; [DebuggerDisplay("{PrintShortSql(), nq}")] public sealed class SelectExpression : Expression, IPrintableExpression { + private static readonly bool UseOldBehavior35476 = + AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue35476", out var enabled35476) && enabled35476; + private IDictionary _projectionMapping = new Dictionary(); private readonly List _sources = []; private readonly List _projection = []; @@ -381,6 +385,12 @@ public void ApplyOffset(SqlExpression sqlExpression) /// public void ApplyOrdering(OrderingExpression orderingExpression) { + if (!UseOldBehavior35476 && orderingExpression is { Expression: SqlFunctionExpression { IsScoringFunction: true }, IsAscending: false }) + { + throw new InvalidOperationException( + CosmosStrings.OrderByDescendingScoringFunction(nameof(Queryable.OrderByDescending), nameof(Queryable.OrderBy))); + } + _orderings.Clear(); _orderings.Add(orderingExpression); } @@ -393,6 +403,19 @@ public void ApplyOrdering(OrderingExpression orderingExpression) /// public void AppendOrdering(OrderingExpression orderingExpression) { + if (!UseOldBehavior35476 && _orderings.Count > 0) + { + var existingScoringFunctionOrdering = _orderings is [{ Expression: SqlFunctionExpression { IsScoringFunction: true } }]; + var appendingScoringFunctionOrdering = orderingExpression.Expression is SqlFunctionExpression { IsScoringFunction: true }; + if (appendingScoringFunctionOrdering || existingScoringFunctionOrdering) + { + throw new InvalidOperationException( + appendingScoringFunctionOrdering && existingScoringFunctionOrdering + ? CosmosStrings.OrderByMultipleScoringFunctionWithoutRrf(nameof(CosmosDbFunctionsExtensions.Rrf)) + : CosmosStrings.OrderByScoringFunctionMixedWithRegularOrderby); + } + } + if (_orderings.FirstOrDefault(o => o.Expression.Equals(orderingExpression.Expression)) == null) { _orderings.Add(orderingExpression); @@ -752,6 +775,11 @@ private void PrintSql(ExpressionPrinter expressionPrinter, bool withTags = true) if (Orderings.Any()) { expressionPrinter.AppendLine().Append("ORDER BY "); + if (!UseOldBehavior35476 && Orderings is [{ Expression: SqlFunctionExpression { IsScoringFunction: true } }]) + { + expressionPrinter.Append("RANK "); + } + expressionPrinter.VisitCollection(Orderings); } diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/SqlFunctionExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/SqlFunctionExpression.cs index 91b53ca7039..960b2c6f0eb 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/SqlFunctionExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/SqlFunctionExpression.cs @@ -3,6 +3,8 @@ // ReSharper disable once CheckNamespace +using System.Diagnostics.CodeAnalysis; + namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; /// @@ -24,10 +26,28 @@ public SqlFunctionExpression( IEnumerable arguments, Type type, CoreTypeMapping? typeMapping) + : this(name, isScoringFunction: false, arguments, type, typeMapping) + { + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + [Experimental(EFDiagnostics.CosmosFullTextSearchExperimental)] + public SqlFunctionExpression( + string name, + bool isScoringFunction, + IEnumerable arguments, + Type type, + CoreTypeMapping? typeMapping) : base(type, typeMapping) { Name = name; Arguments = arguments.ToList(); + IsScoringFunction = isScoringFunction; } /// @@ -38,6 +58,15 @@ public SqlFunctionExpression( /// public virtual string Name { get; } + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + [Experimental(EFDiagnostics.CosmosFullTextSearchExperimental)] + public virtual bool IsScoringFunction { get; } + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -63,7 +92,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) } return changed - ? new SqlFunctionExpression(Name, arguments, Type, TypeMapping) + ? new SqlFunctionExpression(Name, IsScoringFunction, arguments, Type, TypeMapping) : this; } @@ -74,7 +103,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual SqlFunctionExpression ApplyTypeMapping(CoreTypeMapping? typeMapping) - => new(Name, Arguments, Type, typeMapping ?? TypeMapping); + => new(Name, IsScoringFunction, Arguments, Type, typeMapping ?? TypeMapping); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -85,7 +114,7 @@ public virtual SqlFunctionExpression ApplyTypeMapping(CoreTypeMapping? typeMappi public virtual SqlFunctionExpression Update(IReadOnlyList arguments) => arguments.SequenceEqual(Arguments) ? this - : new SqlFunctionExpression(Name, arguments, Type, TypeMapping); + : new SqlFunctionExpression(Name, IsScoringFunction, arguments, Type, TypeMapping); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.Cosmos/Query/Internal/Translators/CosmosFullTextSearchTranslator.cs b/src/EFCore.Cosmos/Query/Internal/Translators/CosmosFullTextSearchTranslator.cs new file mode 100644 index 00000000000..11c4209100e --- /dev/null +++ b/src/EFCore.Cosmos/Query/Internal/Translators/CosmosFullTextSearchTranslator.cs @@ -0,0 +1,116 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Cosmos.Extensions; + +namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; + +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +public class CosmosFullTextSearchTranslator(ISqlExpressionFactory sqlExpressionFactory, ITypeMappingSource typeMappingSource) + : IMethodCallTranslator +{ + private static readonly bool UseOldBehavior35476 = + AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue35476", out var enabled35476) && enabled35476; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public SqlExpression? Translate( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + if (UseOldBehavior35476 || method.DeclaringType != typeof(CosmosDbFunctionsExtensions)) + { + return null; + } + + return method.Name switch + { + nameof(CosmosDbFunctionsExtensions.FullTextContains) + when arguments is [_, var property, var keyword] => sqlExpressionFactory.Function( + "FullTextContains", + [ + property, + keyword, + ], + typeof(bool), + typeMappingSource.FindMapping(typeof(bool))), + + nameof(CosmosDbFunctionsExtensions.FullTextScore) + when arguments is [_, var property, var keywords] => BuildScoringFunction( + sqlExpressionFactory, + "FullTextScore", + [ + property, + keywords, + ], + typeof(double), + typeMappingSource.FindMapping(typeof(double))), + + nameof(CosmosDbFunctionsExtensions.Rrf) + when arguments is [_, ArrayConstantExpression functions] => BuildScoringFunction( + sqlExpressionFactory, + "RRF", + functions.Items, + typeof(double), + typeMappingSource.FindMapping(typeof(double))), + + nameof(CosmosDbFunctionsExtensions.FullTextContainsAny) or nameof(CosmosDbFunctionsExtensions.FullTextContainsAll) + when arguments is [_, SqlExpression property, SqlConstantExpression { Type: var keywordClrType, Value: string[] values } keywords] + && keywordClrType == typeof(string[]) => sqlExpressionFactory.Function( + method.Name == nameof(CosmosDbFunctionsExtensions.FullTextContainsAny) ? "FullTextContainsAny" : "FullTextContainsAll", + [property, .. values.Select(x => sqlExpressionFactory.Constant(x))], + typeof(bool), + typeMappingSource.FindMapping(typeof(bool))), + + nameof(CosmosDbFunctionsExtensions.FullTextContainsAny) or nameof(CosmosDbFunctionsExtensions.FullTextContainsAll) + when arguments is [_, SqlExpression property, SqlParameterExpression { Type: var keywordClrType } keywords] + && keywordClrType == typeof(string[]) => sqlExpressionFactory.Function( + method.Name == nameof(CosmosDbFunctionsExtensions.FullTextContainsAny) ? "FullTextContainsAny" : "FullTextContainsAll", + [property, keywords], + typeof(bool), + typeMappingSource.FindMapping(typeof(bool))), + + nameof(CosmosDbFunctionsExtensions.FullTextContainsAny) or nameof(CosmosDbFunctionsExtensions.FullTextContainsAll) + when arguments is [_, SqlExpression property, ArrayConstantExpression keywords] => sqlExpressionFactory.Function( + method.Name == nameof(CosmosDbFunctionsExtensions.FullTextContainsAny) ? "FullTextContainsAny" : "FullTextContainsAll", + [property, .. keywords.Items], + typeof(bool), + typeMappingSource.FindMapping(typeof(bool))), + + _ => null + }; + } + + private SqlExpression BuildScoringFunction( + ISqlExpressionFactory sqlExpressionFactory, + string functionName, + IEnumerable arguments, + Type returnType, + CoreTypeMapping? typeMapping = null) + { + var typeMappedArguments = new List(); + + foreach (var argument in arguments) + { + typeMappedArguments.Add(argument is SqlExpression sqlArgument ? sqlExpressionFactory.ApplyDefaultTypeMapping(sqlArgument) : argument); + } + + return new SqlFunctionExpression( + functionName, + isScoringFunction: true, + typeMappedArguments, + returnType, + typeMapping); + } +} diff --git a/src/EFCore.Cosmos/Query/Internal/Translators/CosmosVectorSearchTranslator.cs b/src/EFCore.Cosmos/Query/Internal/Translators/CosmosVectorSearchTranslator.cs index 9c6e62d02a2..d77ddbf4373 100644 --- a/src/EFCore.Cosmos/Query/Internal/Translators/CosmosVectorSearchTranslator.cs +++ b/src/EFCore.Cosmos/Query/Internal/Translators/CosmosVectorSearchTranslator.cs @@ -18,6 +18,9 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; public class CosmosVectorSearchTranslator(ISqlExpressionFactory sqlExpressionFactory, ITypeMappingSource typeMappingSource) : IMethodCallTranslator { + private static readonly bool UseOldBehavior35853 = + AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue35853", out var enabled35853) && enabled35853; + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -30,7 +33,15 @@ public class CosmosVectorSearchTranslator(ISqlExpressionFactory sqlExpressionFac IReadOnlyList arguments, IDiagnosticsLogger logger) { - if (method.DeclaringType != typeof(CosmosDbFunctionsExtensions) + if (!UseOldBehavior35853) + { + if (method.DeclaringType != typeof(CosmosDbFunctionsExtensions) + || method.Name != nameof(CosmosDbFunctionsExtensions.VectorDistance)) + { + return null; + } + } + else if (method.DeclaringType != typeof(CosmosDbFunctionsExtensions) && method.Name != nameof(CosmosDbFunctionsExtensions.VectorDistance)) { return null; diff --git a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs index 40573c91264..613f391200a 100644 --- a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs +++ b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs @@ -2549,10 +2549,16 @@ private IReadOnlyList RewriteOperations( var newRawSchema = renameTableOperation.NewSchema; var newSchema = newRawSchema ?? model?.GetDefaultSchema(); + var temporalTableInformation = BuildTemporalInformationFromMigrationOperation(schema, renameTableOperation); if (!temporalTableInformationMap.ContainsKey((tableName, rawSchema))) { - var temporalTableInformation = BuildTemporalInformationFromMigrationOperation(schema, renameTableOperation); temporalTableInformationMap[(tableName, rawSchema)] = temporalTableInformation; + } + + // we still need to check here - table with the new name could have existed before and have been deleted + // we want to preserve the original temporal info of that deleted table + if (!temporalTableInformationMap.ContainsKey((newTableName, newRawSchema))) + { temporalTableInformationMap[(newTableName, newRawSchema)] = temporalTableInformation; } @@ -2647,10 +2653,19 @@ private IReadOnlyList RewriteOperations( var schema = rawSchema ?? model?.GetDefaultSchema(); - // we are guaranteed to find entry here - we looped through all the operations earlier, - // info missing from operations we got from the model - // and in case of no/incomplete model we created dummy (non-temporal) entries - var temporalInformation = temporalTableInformationMap[(tableName, rawSchema)]; + TemporalOperationInformation temporalInformation; + if (operation is CreateTableOperation) + { + // for create table we always generate new temporal information from the operation itself + // just in case there was a table with that name before that got deleted/renamed + // also, temporal state (disabled versioning etc.) should always reset when creating a table + temporalInformation = BuildTemporalInformationFromMigrationOperation(schema, operation); + temporalTableInformationMap[(tableName, rawSchema)] = temporalInformation; + } + else + { + temporalInformation = temporalTableInformationMap[(tableName, rawSchema)]; + } switch (operation) { diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/AdHocCosmosTestHelpers.cs b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocCosmosTestHelpers.cs new file mode 100644 index 00000000000..19a5b39eccc --- /dev/null +++ b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocCosmosTestHelpers.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; +using Microsoft.Azure.Cosmos; +using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class AdHocCosmosTestHelpers +{ + public static async Task CreateCustomEntityHelperAsync( + Container container, + string json, + CancellationToken cancellationToken) + { + var document = JObject.Parse(json); + + var stream = new MemoryStream(); + await using var __ = stream.ConfigureAwait(false); + var writer = new StreamWriter(stream, new UTF8Encoding(), bufferSize: 1024, leaveOpen: false); + await using var ___ = writer.ConfigureAwait(false); + using var jsonWriter = new JsonTextWriter(writer); + + CosmosClientWrapper.Serializer.Serialize(jsonWriter, document); + await jsonWriter.FlushAsync(cancellationToken).ConfigureAwait(false); + + var response = await container.CreateItemStreamAsync( + stream, + PartitionKey.None, + requestOptions: null, + cancellationToken) + .ConfigureAwait(false); + + + if (response.StatusCode != HttpStatusCode.Created) + { + throw new InvalidOperationException($"Failed to create entity (status code: {response.StatusCode}) for json: {json}"); + } + } +} diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/AdHocJsonQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocJsonQueryCosmosTest.cs new file mode 100644 index 00000000000..563d63b7c08 --- /dev/null +++ b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocJsonQueryCosmosTest.cs @@ -0,0 +1,659 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class AdHocJsonQueryCosmosTest : AdHocJsonQueryTestBase +{ + public override async Task Project_root_with_missing_scalars(bool async) + { + if (async) + { + await base.Project_root_with_missing_scalars(async); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (c["Id"] < 4) +"""); + } + } + + [ConditionalTheory(Skip = "issue #35702")] + public override async Task Project_top_level_json_entity_with_missing_scalars(bool async) + { + if (async) + { + await base.Project_top_level_json_entity_with_missing_scalars(async); + + AssertSql(); + } + } + + public override async Task Project_nested_json_entity_with_missing_scalars(bool async) + { + if (async) + { + await AssertTranslationFailed( + () => base.Project_nested_json_entity_with_missing_scalars(async)); + + AssertSql(); + } + } + + [ConditionalTheory(Skip = "issue #34067")] + public override async Task Project_top_level_entity_with_null_value_required_scalars(bool async) + { + if (async) + { + await base.Project_top_level_entity_with_null_value_required_scalars(async); + + AssertSql( + """ +SELECT c["Id"], c +FROM root c +WHERE (c["Id"] = 4) +"""); + } + } + + public override async Task Project_root_entity_with_missing_required_navigation(bool async) + { + if (async) + { + await base.Project_root_entity_with_missing_required_navigation(async); + + AssertSql( + """ +ReadItem(?, ?) +"""); + } + } + + public override async Task Project_missing_required_navigation(bool async) + { + if (async) + { + await base.Project_missing_required_navigation(async); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (c["Id"] = 5) +"""); + } + } + + public override async Task Project_root_entity_with_null_required_navigation(bool async) + { + if (async) + { + await base.Project_root_entity_with_null_required_navigation(async); + + AssertSql( + """ +ReadItem(?, ?) +"""); + } + } + + public override async Task Project_null_required_navigation(bool async) + { + if (async) + { + await base.Project_null_required_navigation(async); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (c["Id"] = 6) +"""); + } + } + + public override async Task Project_missing_required_scalar(bool async) + { + if (async) + { + await base.Project_missing_required_scalar(async); + + AssertSql( + """ +SELECT c["Id"], c["RequiredReference"]["Number"] +FROM root c +WHERE (c["Id"] = 2) +"""); + } + } + + public override async Task Project_null_required_scalar(bool async) + { + if (async) + { + await base.Project_null_required_scalar(async); + + AssertSql( + """ +SELECT c["Id"], c["RequiredReference"]["Number"] +FROM root c +WHERE (c["Id"] = 4) +"""); + } + } + + protected override void OnModelCreating21006(ModelBuilder modelBuilder) + { + base.OnModelCreating21006(modelBuilder); + + modelBuilder.Entity().ToContainer("Entities"); + } + + protected override async Task Seed21006(Context21006 context) + { + await base.Seed21006(context); + + var wrapper = (CosmosClientWrapper)context.GetService(); + var singletonWrapper = context.GetService(); + var entitiesContainer = singletonWrapper.Client.GetContainer(TestStore.Name, containerId: "Entities"); + + var missingTopLevel = +$$""" +{ + "Id": 2, + "$type": "Entity", + "Name": "e2", + "id": "2", + "Collection": [ + { + "Text": "e2 c1", + "NestedCollection": [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 c1 c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 c1 c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 c1 nor" + }, + "NestedRequiredReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 c1 nrr" + } + }, + { + "Text": "e2 c2", + "NestedCollection": [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 c2 c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 c2 c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 c2 nor" + }, + "NestedRequiredReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 c2 nrr" + } + } + ], + "OptionalReference": { + "Text": "e2 or", + "NestedCollection": [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 or c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 or c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 or nor" + }, + "NestedRequiredReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 or nrr" + } + }, + "RequiredReference": { + "Text": "e2 rr", + "NestedCollection": [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 rr c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 rr c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 rr nor" + }, + "NestedRequiredReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 rr nrr" + } + } +} +"""; + + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + entitiesContainer, + missingTopLevel, + CancellationToken.None); + + var missingNested = +$$""" +{ + "Id": 3, + "$type": "Entity", + "Name": "e3", + "id": "3", + "Collection": [ + { + "Number": 7.0, + "Text": "e3 c1", + "NestedCollection": [ + { + "Text": "e3 c1 c1" + }, + { + "Text": "e3 c1 c2" + } + ], + "NestedOptionalReference": { + "Text": "e3 c1 nor" + }, + "NestedRequiredReference": { + "Text": "e3 c1 nrr" + } + }, + { + "Number": 7.0, + "Text": "e3 c2", + "NestedCollection": [ + { + "Text": "e3 c2 c1" + }, + { + "Text": "e3 c2 c2" + } + ], + "NestedOptionalReference": { + "Text": "e3 c2 nor" + }, + "NestedRequiredReference": { + "Text": "e3 c2 nrr" + } + } + ], + "OptionalReference": { + "Number": 7.0, + "Text": "e3 or", + "NestedCollection": [ + { + "Text": "e3 or c1" + }, + { + "Text": "e3 or c2" + } + ], + "NestedOptionalReference": { + "Text": "e3 or nor" + }, + "NestedRequiredReference": { + "Text": "e3 or nrr" + } + }, + "RequiredReference": { + "Number": 7.0, + "Text": "e3 rr", + "NestedCollection": [ + { + "Text": "e3 rr c1" + }, + { + "Text": "e3 rr c2" + } + ], + "NestedOptionalReference": { + "Text": "e3 rr nor" + }, + "NestedRequiredReference": { + "Text": "e3 rr nrr" + } + } +} +"""; + + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + entitiesContainer, + missingNested, + CancellationToken.None); + + var nullTopLevel = +$$""" +{ + "Id": 4, + "$type": "Entity", + "Name": "e4", + "id": "4", + "Collection": [ + { + "Number": null, + "Text": "e4 c1", + "NestedCollection": [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 c1 c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 c1 c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 c1 nor" + }, + "NestedRequiredReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 c1 nrr" + } + }, + { + "Number": null, + "Text": "e4 c2", + "NestedCollection": [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 c2 c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 c2 c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 c2 nor" + }, + "NestedRequiredReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 c2 nrr" + } + } + ], + "OptionalReference": { + "Number": null, + "Text": "e4 or", + "NestedCollection": [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 or c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 or c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 or nor" + }, + "NestedRequiredReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 or nrr" + } + }, + "RequiredReference": { + "Number": null, + "Text": "e4 rr", + "NestedCollection": [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 rr c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 rr c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 rr nor" + }, + "NestedRequiredReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 rr nrr" + } + } +} +"""; + + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + entitiesContainer, + nullTopLevel, + CancellationToken.None); + + var missingRequiredNav = +$$""" +{ + "Id": 5, + "$type": "Entity", + "Name": "e5", + "id": "5", + "Collection": [ + { + "Number": 7.0, + "Text": "e5 c1", + "NestedCollection": [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e5 c1 c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e5 c1 c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e5 c1 nor" + }, + }, + { + "Number": 7.0, + "Text": "e5 c2", + "NestedCollection": [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e5 c2 c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e5 c2 c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e5 c2 nor" + }, + } + ], + "OptionalReference": { + "Number": 7.0, + "Text": "e5 or", + "NestedCollection": [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e5 or c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e5 or c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e5 or nor" + }, + }, + "RequiredReference": { + "Number": 7.0, + "Text": "e5 rr", + "NestedCollection": [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e5 rr c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e5 rr c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e5 rr nor" + }, + } +} +"""; + + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + entitiesContainer, + missingRequiredNav, + CancellationToken.None); + + var nullRequiredNav = +$$""" +{ + "Id": 6, + "$type": "Entity", + "Name": "e6", + "id": "6", + "Collection": [ + { + "Number": 7.0, + "Text": "e6 c1", + "NestedCollection": [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e6 c1 c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e6 c1 c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e6 c1 nor" + }, + "NestedRequiredReference": null + }, + { + "Number": 7.0, + "Text": "e6 c2", + "NestedCollection": [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e6 c2 c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e6 c2 c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e6 c2 nor" + }, + "NestedRequiredReference": null + } + ], + "OptionalReference": { + "Number": 7.0, + "Text": "e6 or", + "NestedCollection": [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e6 or c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e6 or c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e6 or nor" + }, + "NestedRequiredReference": null + }, + "RequiredReference": { + "Number": 7.0, + "Text": "e6 rr", + "NestedCollection": [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e6 rr c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e6 rr c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e6 rr nor" + }, + "NestedRequiredReference": null + } +} +"""; + + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + entitiesContainer, + nullRequiredNav, + CancellationToken.None); + } + + protected TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + private void AssertSql(params string[] expected) + => TestSqlLoggerFactory.AssertBaseline(expected); + + protected static async Task AssertTranslationFailed(Func query) + => Assert.Contains( + CoreStrings.TranslationFailed("")[48..], + (await Assert.ThrowsAsync(query)) + .Message); + + protected override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + => builder.ConfigureWarnings(b => b.Ignore(CosmosEventId.NoPartitionKeyDefined)); + + protected override ITestStoreFactory TestStoreFactory + => CosmosTestStoreFactory.Instance; +} diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/AdHocMiscellaneousQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocMiscellaneousQueryCosmosTest.cs index e2dd2b90904..fb9c9bc95f7 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/AdHocMiscellaneousQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocMiscellaneousQueryCosmosTest.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; namespace Microsoft.EntityFrameworkCore.Query; @@ -9,6 +10,144 @@ namespace Microsoft.EntityFrameworkCore.Query; public class AdHocMiscellaneousQueryCosmosTest : NonSharedModelTestBase { + #region 21006 + + [ConditionalFact] + public virtual async Task Project_all_types_entity_with_missing_scalars() + { + var contextFactory = await InitializeAsync( + onModelCreating: OnModelCreating21006, + seed: Seed21006); + + await using var context = contextFactory.CreateContext(); + + var query = context.Set(); + + var result = await query.ToListAsync(); + } + + public void OnModelCreating21006(ModelBuilder modelBuilder) + { + modelBuilder.Entity(b => + { + b.Property(x => x.Id).ValueGeneratedNever(); + b.ToContainer("Entities"); + b.Property(x => x.TestDecimal).HasPrecision(18, 3); + b.OwnsOne(x => x.Reference, bb => + { + bb.Property(x => x.TestDecimal).HasPrecision(18, 3); + bb.Property(x => x.TestEnumWithIntConverter).HasConversion(); + }); + }); + } + + protected async Task Seed21006(JsonContext21006 context) + { + var wrapper = (CosmosClientWrapper)context.GetService(); + var singletonWrapper = context.GetService(); + var entitiesContainer = singletonWrapper.Client.GetContainer(TestStore.Name, containerId: "Entities"); + + var missingTopLevel = +$$""" +{ + "Id": 1, + "$type": "Entity", + "id": "1", + "Reference": { + "Text": "e2 or" + } +} +"""; + + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + entitiesContainer, + missingTopLevel, + CancellationToken.None); + } + + protected class JsonContext21006(DbContextOptions options) : DbContext(options) + { + public DbSet Entities { get; set; } + + public class Entity + { + public int Id { get; set; } + + public short TestInt16 { get; set; } + public int TestInt32 { get; set; } + public long TestInt64 { get; set; } + public double TestDouble { get; set; } + public decimal TestDecimal { get; set; } + public DateTime TestDateTime { get; set; } + public DateTimeOffset TestDateTimeOffset { get; set; } + public TimeSpan TestTimeSpan { get; set; } + public DateOnly TestDateOnly { get; set; } + public TimeOnly TestTimeOnly { get; set; } + public float TestSingle { get; set; } + public bool TestBoolean { get; set; } + public byte TestByte { get; set; } + + public byte[] TestByteArray { get; set; } + public Guid TestGuid { get; set; } + public ushort TestUnsignedInt16 { get; set; } + public uint TestUnsignedInt32 { get; set; } + public ulong TestUnsignedInt64 { get; set; } + public char TestCharacter { get; set; } + public sbyte TestSignedByte { get; set; } + public int? TestNullableInt32 { get; set; } + public JsonEnum TestEnum { get; set; } + public byte[] TestByteCollection { get; set; } + public IList TestUnsignedInt16Collection { get; set; } + public uint[] TestUnsignedInt32Collection { get; set; } + public sbyte[] TestSignedByteCollection { get; set; } + public JsonEntity Reference { get; set; } + } + + public class JsonEntity + { + public string Text { get; set; } + + public short TestInt16 { get; set; } + public int TestInt32 { get; set; } + public long TestInt64 { get; set; } + public double TestDouble { get; set; } + public decimal TestDecimal { get; set; } + public DateTime TestDateTime { get; set; } + public DateTimeOffset TestDateTimeOffset { get; set; } + public TimeSpan TestTimeSpan { get; set; } + public DateOnly TestDateOnly { get; set; } + public TimeOnly TestTimeOnly { get; set; } + public float TestSingle { get; set; } + public bool TestBoolean { get; set; } + public byte TestByte { get; set; } + public byte[] TestByteArray { get; set; } + public Guid TestGuid { get; set; } + public ushort TestUnsignedInt16 { get; set; } + public uint TestUnsignedInt32 { get; set; } + public ulong TestUnsignedInt64 { get; set; } + public char TestCharacter { get; set; } + public sbyte TestSignedByte { get; set; } + public int? TestNullableInt32 { get; set; } + public JsonEnum TestEnum { get; set; } + public JsonEnum TestEnumWithIntConverter { get; set; } + + public byte[] TestByteCollection { get; set; } + public IList TestUnsignedInt16Collection { get; set; } + public uint[] TestUnsignedInt32Collection { get; set; } + + public sbyte[] TestSignedByteCollection { get; set; } + } + + public enum JsonEnum + { + One = -1, + Two = 2, + Three = -3 + } + } + + #endregion + #region 34911 [ConditionalFact] diff --git a/test/EFCore.InMemory.FunctionalTests/InMemoryComplianceTest.cs b/test/EFCore.InMemory.FunctionalTests/InMemoryComplianceTest.cs index dccb622b444..6a0766d9844 100644 --- a/test/EFCore.InMemory.FunctionalTests/InMemoryComplianceTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/InMemoryComplianceTest.cs @@ -25,6 +25,7 @@ public class InMemoryComplianceTest : ComplianceTestBase typeof(NonSharedModelBulkUpdatesTestBase), typeof(NorthwindBulkUpdatesTestBase<>), typeof(JsonQueryTestBase<>), + typeof(AdHocJsonQueryTestBase), }; protected override Assembly TargetAssembly { get; } = typeof(InMemoryComplianceTest).Assembly; diff --git a/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsTestBase.cs b/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsTestBase.cs index f83e2575b6b..d91dafb6d63 100644 --- a/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsTestBase.cs @@ -3213,6 +3213,118 @@ public virtual Task Add_required_primitve_collection_with_custom_converter_and_c Assert.Single(customersTable.PrimaryKey!.Columns)); }); + [ConditionalFact] + public virtual Task Multiop_drop_table_and_create_the_same_table_in_one_migration() + => TestComposite( + [ + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.HasKey("Id"); + e.ToTable("Customers"); + }), + builder => { }, + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.HasKey("Id"); + + e.ToTable("Customers"); + }) + ]); + + [ConditionalFact] + public virtual Task Multiop_create_table_and_drop_it_in_one_migration() + => TestComposite( + [ + builder => { }, + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.HasKey("Id"); + + e.ToTable("Customers"); + }), + builder => { }, + ]); + + [ConditionalFact] + public virtual Task Multiop_rename_table_and_drop() + => TestComposite( + [ + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.HasKey("Id"); + + e.ToTable("Customers"); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.HasKey("Id"); + + e.ToTable("NewCustomers"); + }), + builder => { }, + ]); + + [ConditionalFact] + public virtual Task Multiop_rename_table_and_create_new_table_with_the_old_name() + => TestComposite( + [ + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.HasKey("Id"); + + e.ToTable("Customers"); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.HasKey("Id"); + + e.ToTable("NewCustomers"); + }), + builder => + { + builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.HasKey("Id"); + + e.ToTable("NewCustomers"); + }); + + builder.Entity( + "AnotherCustomer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.HasKey("Id"); + + e.ToTable("Customers"); + }); + }, + ]); + protected class Person { public int Id { get; set; } @@ -3262,6 +3374,43 @@ protected virtual Task Test( MigrationsSqlGenerationOptions migrationsSqlGenerationOptions = MigrationsSqlGenerationOptions.Default) => Test(_ => { }, buildSourceAction, buildTargetAction, asserter, withConventions, migrationsSqlGenerationOptions); + protected virtual Task TestComposite( + List> buildActions, + bool withConventions = true, + MigrationsSqlGenerationOptions migrationsSqlGenerationOptions = MigrationsSqlGenerationOptions.Default) + { + if (buildActions.Count < 3) + { + throw new InvalidOperationException("You need at least 3 build actions for the composite case."); + } + + var context = CreateContext(); + var modelDiffer = context.GetService(); + var modelRuntimeInitializer = context.GetService(); + + var models = new List(); + for (var i = 0; i < buildActions.Count; i++) + { + var modelBuilder = CreateModelBuilder(withConventions); + buildActions[i](modelBuilder); + + var preSnapshotModel = modelRuntimeInitializer.Initialize( + (IModel)modelBuilder.Model, designTime: true, validationLogger: null); + + models.Add(preSnapshotModel); + } + + // build all migration operations going through each intermediate state of the model + var operations = new List(); + for (var i = 0; i < models.Count - 1; i++) + { + operations.AddRange( + modelDiffer.GetDifferences(models[i].GetRelationalModel(), models[i + 1].GetRelationalModel())); + } + + return Test(models.First(), models.Last(), operations, null, migrationsSqlGenerationOptions); + } + protected virtual Task Test( Action buildCommonAction, Action buildSourceAction, diff --git a/test/EFCore.Relational.Specification.Tests/Query/AdHocJsonQueryTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/AdHocJsonQueryRelationalTestBase.cs similarity index 95% rename from test/EFCore.Relational.Specification.Tests/Query/AdHocJsonQueryTestBase.cs rename to test/EFCore.Relational.Specification.Tests/Query/AdHocJsonQueryRelationalTestBase.cs index d7461ae71fe..7d0d00e4a56 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/AdHocJsonQueryTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/AdHocJsonQueryRelationalTestBase.cs @@ -5,15 +5,50 @@ namespace Microsoft.EntityFrameworkCore.Query; #nullable disable -public abstract class AdHocJsonQueryTestBase : NonSharedModelTestBase +public abstract class AdHocJsonQueryRelationalTestBase : AdHocJsonQueryTestBase { - protected override string StoreName - => "AdHocJsonQueryTest"; + #region 21006 - protected virtual void ConfigureWarnings(WarningsConfigurationBuilder builder) + public override async Task Project_missing_required_navigation(bool async) { + var message = (await Assert.ThrowsAsync( + () => base.Project_missing_required_navigation(async))).Message; + + Assert.Equal(RelationalStrings.JsonRequiredEntityWithNullJson(typeof(Context21006.JsonEntityNested).Name), message); + } + + public override async Task Project_null_required_navigation(bool async) + { + var message = (await Assert.ThrowsAsync( + () => base.Project_null_required_navigation(async))).Message; + + Assert.Equal(RelationalStrings.JsonRequiredEntityWithNullJson(typeof(Context21006.JsonEntityNested).Name), message); } + public override async Task Project_top_level_entity_with_null_value_required_scalars(bool async) + { + var message = (await Assert.ThrowsAsync( + () => base.Project_top_level_entity_with_null_value_required_scalars(async))).Message; + + Assert.Equal("Cannot get the value of a token type 'Null' as a number.", message); + } + + protected override void OnModelCreating21006(ModelBuilder modelBuilder) + { + base.OnModelCreating21006(modelBuilder); + + modelBuilder.Entity( + b => + { + b.ToTable("Entities"); + b.OwnsOne(x => x.OptionalReference).ToJson(); + b.OwnsOne(x => x.RequiredReference).ToJson(); + b.OwnsMany(x => x.Collection).ToJson(); + }); + } + + #endregion + #region 32310 [ConditionalTheory] diff --git a/test/EFCore.Specification.Tests/Query/AdHocJsonQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/AdHocJsonQueryTestBase.cs new file mode 100644 index 00000000000..820b7acc872 --- /dev/null +++ b/test/EFCore.Specification.Tests/Query/AdHocJsonQueryTestBase.cs @@ -0,0 +1,419 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query; + +#nullable disable + +public abstract class AdHocJsonQueryTestBase : NonSharedModelTestBase +{ + protected override string StoreName + => "AdHocJsonQueryTests"; + + protected virtual void ClearLog() + => ListLoggerFactory.Clear(); + + protected virtual void ConfigureWarnings(WarningsConfigurationBuilder builder) + { + } + + #region 21006 + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Project_root_with_missing_scalars(bool async) + { + var contextFactory = await InitializeAsync( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + onModelCreating: OnModelCreating21006, + seed: Seed21006); + + await using var context = contextFactory.CreateContext(); + + var query = context.Set().Where(x => x.Id < 4); + + var result = async + ? await query.ToListAsync() + : query.ToList()!; + + var topLevel = result.Single(x => x.Id == 2); + var nested = result.Single(x => x.Id == 3); + + Assert.Equal(default, topLevel.OptionalReference.Number); + Assert.Equal(default, topLevel.RequiredReference.Number); + Assert.True(topLevel.Collection.All(x => x.Number == default)); + + Assert.Equal(default, nested.RequiredReference.NestedRequiredReference.DoB); + Assert.Equal(default, nested.RequiredReference.NestedOptionalReference.DoB); + Assert.Equal(default, nested.OptionalReference.NestedRequiredReference.DoB); + Assert.Equal(default, nested.OptionalReference.NestedOptionalReference.DoB); + Assert.True(nested.Collection.SelectMany(x => x.NestedCollection).All(x => x.DoB == default)); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Project_top_level_json_entity_with_missing_scalars(bool async) + { + var contextFactory = await InitializeAsync( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + onModelCreating: OnModelCreating21006, + seed: Seed21006); + + await using var context = contextFactory.CreateContext(); + + var query = context.Set().Where(x => x.Id < 4).Select(x => new + { + x.Id, + x.OptionalReference, + x.RequiredReference, + x.Collection + }).AsNoTracking(); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + var topLevel = result.Single(x => x.Id == 2); + var nested = result.Single(x => x.Id == 3); + + Assert.Equal(default, topLevel.OptionalReference.Number); + Assert.Equal(default, topLevel.RequiredReference.Number); + Assert.True(topLevel.Collection.All(x => x.Number == default)); + + Assert.Equal(default, nested.RequiredReference.NestedRequiredReference.DoB); + Assert.Equal(default, nested.RequiredReference.NestedOptionalReference.DoB); + Assert.Equal(default, nested.OptionalReference.NestedRequiredReference.DoB); + Assert.Equal(default, nested.OptionalReference.NestedOptionalReference.DoB); + Assert.True(nested.Collection.SelectMany(x => x.NestedCollection).All(x => x.DoB == default)); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Project_nested_json_entity_with_missing_scalars(bool async) + { + var contextFactory = await InitializeAsync( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + onModelCreating: OnModelCreating21006, + seed: Seed21006); + + await using var context = contextFactory.CreateContext(); + + var query = context.Set().Where(x => x.Id < 4).Select(x => new + { + x.Id, + x.OptionalReference.NestedOptionalReference, + x.RequiredReference.NestedRequiredReference, + x.Collection[0].NestedCollection + }).AsNoTracking(); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + var topLevel = result.Single(x => x.Id == 2); + var nested = result.Single(x => x.Id == 3); + + Assert.Equal(default, nested.NestedOptionalReference.DoB); + Assert.Equal(default, nested.NestedRequiredReference.DoB); + Assert.True(nested.NestedCollection.All(x => x.DoB == default)); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Project_top_level_entity_with_null_value_required_scalars(bool async) + { + var contextFactory = await InitializeAsync( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + onModelCreating: OnModelCreating21006, + seed: Seed21006); + + await using var context = contextFactory.CreateContext(); + + var query = context.Set().Where(x => x.Id == 4).Select(x => new + { + x.Id, + x.RequiredReference, + }).AsNoTracking(); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + var nullScalars = result.Single(); + + Assert.Equal(default, nullScalars.RequiredReference.Number); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Project_root_entity_with_missing_required_navigation(bool async) + { + var contextFactory = await InitializeAsync( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + onModelCreating: OnModelCreating21006, + seed: Seed21006); + + await using var context = contextFactory.CreateContext(); + + var query = context.Set().Where(x => x.Id == 5).AsNoTracking(); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + var missingRequiredNav = result.Single(); + + Assert.Equal(default, missingRequiredNav.RequiredReference.NestedRequiredReference); + Assert.Equal(default, missingRequiredNav.OptionalReference.NestedRequiredReference); + Assert.True(missingRequiredNav.Collection.All(x => x.NestedRequiredReference == default)); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Project_missing_required_navigation(bool async) + { + var contextFactory = await InitializeAsync( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + onModelCreating: OnModelCreating21006, + seed: Seed21006); + + await using var context = contextFactory.CreateContext(); + + var query = context.Set().Where(x => x.Id == 5).Select(x => x.RequiredReference.NestedRequiredReference).AsNoTracking(); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + var missingRequiredNav = result.Single(); + + Assert.Equal(default, missingRequiredNav); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Project_root_entity_with_null_required_navigation(bool async) + { + var contextFactory = await InitializeAsync( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + onModelCreating: OnModelCreating21006, + seed: Seed21006); + + await using var context = contextFactory.CreateContext(); + + var query = context.Set().Where(x => x.Id == 6).AsNoTracking(); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + var nullRequiredNav = result.Single(); + + Assert.Equal(default, nullRequiredNav.RequiredReference.NestedRequiredReference); + Assert.Equal(default, nullRequiredNav.OptionalReference.NestedRequiredReference); + Assert.True(nullRequiredNav.Collection.All(x => x.NestedRequiredReference == default)); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Project_null_required_navigation(bool async) + { + var contextFactory = await InitializeAsync( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + onModelCreating: OnModelCreating21006, + seed: Seed21006); + + await using var context = contextFactory.CreateContext(); + + var query = context.Set().Where(x => x.Id == 6).Select(x => x.RequiredReference).AsNoTracking(); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + var nullRequiredNav = result.Single(); + + Assert.Equal(default, nullRequiredNav.NestedRequiredReference); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Project_missing_required_scalar(bool async) + { + var contextFactory = await InitializeAsync( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + onModelCreating: OnModelCreating21006, + seed: Seed21006); + + await using var context = contextFactory.CreateContext(); + + var query = context.Set() + .Where(x => x.Id == 2) + .Select(x => new + { + x.Id, + Number = (double?)x.RequiredReference.Number + }); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + Assert.Null(result.Single().Number); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Project_null_required_scalar(bool async) + { + var contextFactory = await InitializeAsync( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + onModelCreating: OnModelCreating21006, + seed: Seed21006); + + await using var context = contextFactory.CreateContext(); + + var query = context.Set() + .Where(x => x.Id == 4) + .Select(x => new + { + x.Id, + Number = (double?)x.RequiredReference.Number, + }); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + Assert.Null(result.Single().Number); + } + + protected virtual void OnModelCreating21006(ModelBuilder modelBuilder) + => modelBuilder.Entity( + b => + { + b.Property(x => x.Id).ValueGeneratedNever(); + b.OwnsOne( + x => x.OptionalReference, bb => + { + bb.OwnsOne(x => x.NestedOptionalReference); + bb.OwnsOne(x => x.NestedRequiredReference); + bb.Navigation(x => x.NestedRequiredReference).IsRequired(); + bb.OwnsMany(x => x.NestedCollection); + }); + b.OwnsOne( + x => x.RequiredReference, bb => + { + bb.OwnsOne(x => x.NestedOptionalReference); + bb.OwnsOne(x => x.NestedRequiredReference); + bb.Navigation(x => x.NestedRequiredReference).IsRequired(); + bb.OwnsMany(x => x.NestedCollection); + }); + b.Navigation(x => x.RequiredReference).IsRequired(); + b.OwnsMany( + x => x.Collection, bb => + { + bb.OwnsOne(x => x.NestedOptionalReference); + bb.OwnsOne(x => x.NestedRequiredReference); + bb.Navigation(x => x.NestedRequiredReference).IsRequired(); + bb.OwnsMany(x => x.NestedCollection); + }); + }); + + protected virtual async Task Seed21006(Context21006 context) + { + // everything + var e1 = new Context21006.Entity + { + Id = 1, + Name = "e1", + OptionalReference = new Context21006.JsonEntity + { + Number = 7, + Text = "e1 or", + NestedOptionalReference = new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 or nor" }, + NestedRequiredReference = new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 or nrr" }, + NestedCollection = new List + { + new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 or c1" }, + new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 or c2" }, + } + }, + + RequiredReference = new Context21006.JsonEntity + { + Number = 7, + Text = "e1 rr", + NestedOptionalReference = new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 rr nor" }, + NestedRequiredReference = new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 rr nrr" }, + NestedCollection = new List + { + new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 rr c1" }, + new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 rr c2" }, + } + }, + Collection = new List + { + new Context21006.JsonEntity + { + Number = 7, + Text = "e1 c1", + NestedOptionalReference = new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 c1 nor" }, + NestedRequiredReference = new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 c1 nrr" }, + NestedCollection = new List + { + new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 c1 c1" }, + new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 c1 c2" }, + } + }, + new Context21006.JsonEntity + { + Number = 7, + Text = "e1 c2", + NestedOptionalReference = new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 c2 nor" }, + NestedRequiredReference = new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 c2 nrr" }, + NestedCollection = new List + { + new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 c2 c1" }, + new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 c2 c2" }, + } + }, + } + }; + + context.Add(e1); + await context.SaveChangesAsync(); + } + + protected class Context21006(DbContextOptions options) : DbContext(options) + { + public DbSet Entities { get; set; } + + public class Entity + { + public int Id { get; set; } + public string Name { get; set; } + public JsonEntity OptionalReference { get; set; } + public JsonEntity RequiredReference { get; set; } + public List Collection { get; set; } + } + + public class JsonEntity + { + public string Text { get; set; } + public double Number { get; set; } + + public JsonEntityNested NestedOptionalReference { get; set; } + public JsonEntityNested NestedRequiredReference { get; set; } + public List NestedCollection { get; set; } + } + + public class JsonEntityNested + { + public DateTime DoB { get; set; } + public string Text { get; set; } + } + } + + #endregion +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs index b7998252e12..b1931dad046 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs @@ -3440,6 +3440,90 @@ await Test( """); } + public override async Task Multiop_drop_table_and_create_the_same_table_in_one_migration() + { + await base.Multiop_drop_table_and_create_the_same_table_in_one_migration(); + + AssertSql( + """ +DROP TABLE [Customers]; +""", + // + """ +CREATE TABLE [Customers] ( + [Id] int NOT NULL IDENTITY, + [Name] nvarchar(max) NULL, + CONSTRAINT [PK_Customers] PRIMARY KEY ([Id]) +); +"""); + } + + public override async Task Multiop_create_table_and_drop_it_in_one_migration() + { + await base.Multiop_create_table_and_drop_it_in_one_migration(); + + AssertSql( + """ +CREATE TABLE [Customers] ( + [Id] int NOT NULL IDENTITY, + [Name] nvarchar(max) NULL, + CONSTRAINT [PK_Customers] PRIMARY KEY ([Id]) +); +""", + // + """ +DROP TABLE [Customers]; +"""); + } + + public override async Task Multiop_rename_table_and_drop() + { + await base.Multiop_rename_table_and_drop(); + + AssertSql( + """ +ALTER TABLE [Customers] DROP CONSTRAINT [PK_Customers]; +""", + // + """ +EXEC sp_rename N'[Customers]', N'NewCustomers', 'OBJECT'; +""", + // + """ +ALTER TABLE [NewCustomers] ADD CONSTRAINT [PK_NewCustomers] PRIMARY KEY ([Id]); +""", + // + """ +DROP TABLE [NewCustomers]; +"""); + } + + public override async Task Multiop_rename_table_and_create_new_table_with_the_old_name() + { + await base.Multiop_rename_table_and_create_new_table_with_the_old_name(); + + AssertSql( + """ +ALTER TABLE [Customers] DROP CONSTRAINT [PK_Customers]; +""", + // + """ +EXEC sp_rename N'[Customers]', N'NewCustomers', 'OBJECT'; +""", + // + """ +ALTER TABLE [NewCustomers] ADD CONSTRAINT [PK_NewCustomers] PRIMARY KEY ([Id]); +""", + // + """ +CREATE TABLE [Customers] ( + [Id] int NOT NULL IDENTITY, + [Name] nvarchar(max) NULL, + CONSTRAINT [PK_Customers] PRIMARY KEY ([Id]) +); +"""); + } + [ConditionalFact] public virtual async Task Create_temporal_table_default_column_mappings_and_default_history_table() { @@ -10757,6 +10841,363 @@ CONSTRAINT [PK_HistoryTable] PRIMARY KEY ([Id]) """); } + [ConditionalFact] + public virtual async Task Temporal_multiop_drop_temporal_table_and_add_the_same_table_in_one_migration() + { + await TestComposite( + [ + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate(); + e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable", "historySchema"); + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + })); + }), + builder => { }, + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate(); + e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable", "historySchema"); + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + })); + }) + ]); + + AssertSql( +""" +ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +DROP TABLE [Customers]; +""", + // + """ +DROP TABLE [historySchema].[HistoryTable]; +""", + // + """ +IF SCHEMA_ID(N'historySchema') IS NULL EXEC(N'CREATE SCHEMA [historySchema];'); +""", + // + """ +CREATE TABLE [Customers] ( + [Id] int NOT NULL IDENTITY, + [Name] nvarchar(max) NULL, + [SystemTimeEnd] datetime2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL, + [SystemTimeStart] datetime2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL, + CONSTRAINT [PK_Customers] PRIMARY KEY ([Id]), + PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd]) +) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [historySchema].[HistoryTable])); +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_multiop_create_temporal_and_drop() + { + await TestComposite( + [ + builder => { }, + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate(); + e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable", "historySchema"); + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + })); + }), + builder => { }, + ]); + + AssertSql( +""" +IF SCHEMA_ID(N'historySchema') IS NULL EXEC(N'CREATE SCHEMA [historySchema];'); +""", + // + """ +CREATE TABLE [Customers] ( + [Id] int NOT NULL IDENTITY, + [Name] nvarchar(max) NULL, + [SystemTimeEnd] datetime2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL, + [SystemTimeStart] datetime2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL, + CONSTRAINT [PK_Customers] PRIMARY KEY ([Id]), + PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd]) +) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [historySchema].[HistoryTable])); +""", + // + """ +ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +DROP TABLE [Customers]; +""", + // + """ +DROP TABLE [historySchema].[HistoryTable]; +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_multiop_rename_temporal_and_drop() + { + await TestComposite( + [ + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate(); + e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable", "historySchema"); + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate(); + e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + "NewCustomers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable", "historySchema"); + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + })); + }), + builder => { }, + ]); + + AssertSql( +""" +ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +ALTER TABLE [Customers] DROP CONSTRAINT [PK_Customers]; +""", + // + """ +EXEC sp_rename N'[Customers]', N'NewCustomers', 'OBJECT'; +""", + // + """ +ALTER TABLE [NewCustomers] ADD CONSTRAINT [PK_NewCustomers] PRIMARY KEY ([Id]); +""", + // + """ +DROP TABLE [NewCustomers]; +""", + // + """ +DROP TABLE [historySchema].[HistoryTable]; +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_multiop_rename_period_drop_table_create_as_regular() + { + await TestComposite( + [ + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate(); + e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable", "historySchema"); + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("NewSystemTimeStart").ValueGeneratedOnAddOrUpdate(); + e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable", "historySchema"); + ttb.HasPeriodStart("NewSystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + })); + }), + builder => { }, + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.HasKey("Id"); + + e.ToTable("Customers"); + }), + ]); + + AssertSql( +""" +EXEC sp_rename N'[Customers].[SystemTimeStart]', N'NewSystemTimeStart', 'COLUMN'; +""", + // + """ +ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +DROP TABLE [Customers]; +""", + // + """ +DROP TABLE [historySchema].[HistoryTable]; +""", + // + """ +CREATE TABLE [Customers] ( + [Id] int NOT NULL IDENTITY, + [Name] nvarchar(max) NULL, + CONSTRAINT [PK_Customers] PRIMARY KEY ([Id]) +); +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_multiop_rename_column_drop_table_create_as_regular() + { + await TestComposite( + [ + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate(); + e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable", "historySchema"); + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("NewName"); + e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate(); + e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable", "historySchema"); + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + })); + }), + builder => { }, + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.HasKey("Id"); + + e.ToTable("Customers"); + }), + ]); + + AssertSql( +""" +EXEC sp_rename N'[Customers].[Name]', N'NewName', 'COLUMN'; +""", + // + """ +ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +DROP TABLE [Customers]; +""", + // + """ +DROP TABLE [historySchema].[HistoryTable]; +""", + // + """ +CREATE TABLE [Customers] ( + [Id] int NOT NULL IDENTITY, + [Name] nvarchar(max) NULL, + CONSTRAINT [PK_Customers] PRIMARY KEY ([Id]) +); +"""); + } + [ConditionalFact] public override async Task Add_required_primitive_collection_to_existing_table() { diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/AdHocJsonQuerySqlServerTestBase.cs b/test/EFCore.SqlServer.FunctionalTests/Query/AdHocJsonQuerySqlServerTestBase.cs index ee179b3d6f4..0143152033d 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/AdHocJsonQuerySqlServerTestBase.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/AdHocJsonQuerySqlServerTestBase.cs @@ -8,7 +8,7 @@ namespace Microsoft.EntityFrameworkCore.Query; -public abstract class AdHocJsonQuerySqlServerTestBase : AdHocJsonQueryTestBase +public abstract class AdHocJsonQuerySqlServerTestBase : AdHocJsonQueryRelationalTestBase { protected override ITestStoreFactory TestStoreFactory => SqlServerTestStoreFactory.Instance; @@ -20,6 +20,184 @@ protected override void ConfigureWarnings(WarningsConfigurationBuilder builder) builder.Log(CoreEventId.StringEnumValueInJson, SqlServerEventId.JsonTypeExperimental); } + protected void AssertSql(params string[] expected) + => TestSqlLoggerFactory.AssertBaseline(expected); + + public override async Task Project_root_with_missing_scalars(bool async) + { + await base.Project_root_with_missing_scalars(async); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], [e].[Collection], [e].[OptionalReference], [e].[RequiredReference] +FROM [Entities] AS [e] +WHERE [e].[Id] < 4 +"""); + } + + public override async Task Project_top_level_json_entity_with_missing_scalars(bool async) + { + await base.Project_top_level_json_entity_with_missing_scalars(async); + + AssertSql( + """ +SELECT [e].[Id], [e].[OptionalReference], [e].[RequiredReference], [e].[Collection] +FROM [Entities] AS [e] +WHERE [e].[Id] < 4 +"""); + } + + public override async Task Project_nested_json_entity_with_missing_scalars(bool async) + { + await base.Project_nested_json_entity_with_missing_scalars(async); + + AssertSql( +""" +SELECT [e].[Id], JSON_QUERY([e].[OptionalReference], '$.NestedOptionalReference'), JSON_QUERY([e].[RequiredReference], '$.NestedRequiredReference'), JSON_QUERY([e].[Collection], '$[0].NestedCollection') +FROM [Entities] AS [e] +WHERE [e].[Id] < 4 +"""); + } + + public override async Task Project_root_entity_with_missing_required_navigation(bool async) + { + await base.Project_root_entity_with_missing_required_navigation(async); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], [e].[Collection], [e].[OptionalReference], [e].[RequiredReference] +FROM [Entities] AS [e] +WHERE [e].[Id] = 5 +"""); + } + + + public override async Task Project_missing_required_navigation(bool async) + { + await base.Project_missing_required_navigation(async); + + AssertSql( + """ +SELECT JSON_QUERY([e].[RequiredReference], '$.NestedRequiredReference'), [e].[Id] +FROM [Entities] AS [e] +WHERE [e].[Id] = 5 +"""); + } + + public override async Task Project_root_entity_with_null_required_navigation(bool async) + { + await base.Project_root_entity_with_null_required_navigation(async); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], [e].[Collection], [e].[OptionalReference], [e].[RequiredReference] +FROM [Entities] AS [e] +WHERE [e].[Id] = 6 +"""); + } + + public override async Task Project_null_required_navigation(bool async) + { + await base.Project_null_required_navigation(async); + + AssertSql( + """ +SELECT [e].[RequiredReference], [e].[Id] +FROM [Entities] AS [e] +WHERE [e].[Id] = 6 +"""); + } + + public override async Task Project_missing_required_scalar(bool async) + { + await base.Project_missing_required_scalar(async); + + AssertSql( + """ +SELECT [e].[Id], CAST(JSON_VALUE([e].[RequiredReference], '$.Number') AS float) AS [Number] +FROM [Entities] AS [e] +WHERE [e].[Id] = 2 +"""); + } + + public override async Task Project_null_required_scalar(bool async) + { + await base.Project_null_required_scalar(async); + + AssertSql( + """ +SELECT [e].[Id], CAST(JSON_VALUE([e].[RequiredReference], '$.Number') AS float) AS [Number] +FROM [Entities] AS [e] +WHERE [e].[Id] = 4 +"""); + } + + protected override async Task Seed21006(Context21006 context) + { + await base.Seed21006(context); + + // missing scalar on top level + await context.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO [Entities] ([Collection], [OptionalReference], [RequiredReference], [Id], [Name]) +VALUES ( +N'[{"Text":"e2 c1","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e2 c1 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e2 c1 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 c1 nor"},"NestedRequiredReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 c1 nrr"}},{"Text":"e2 c2","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e2 c2 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e2 c2 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 c2 nor"},"NestedRequiredReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 c2 nrr"}}]', +N'{"Text":"e2 or","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e2 or c1"},{"DoB":"2000-01-01T00:00:00","Text":"e2 or c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 or nor"},"NestedRequiredReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 or nrr"}}', +N'{"Text":"e2 rr","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e2 rr c1"},{"DoB":"2000-01-01T00:00:00","Text":"e2 rr c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 rr nor"},"NestedRequiredReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 rr nrr"}}', +2, +N'e2') +"""); + + // missing scalar on nested level + await context.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO [Entities] ([Collection], [OptionalReference], [RequiredReference], [Id], [Name]) +VALUES ( +N'[{"Number":7,"Text":"e3 c1","NestedCollection":[{"Text":"e3 c1 c1"},{"Text":"e3 c1 c2"}],"NestedOptionalReference":{"Text":"e3 c1 nor"},"NestedRequiredReference":{"Text":"e3 c1 nrr"}},{"Number":7,"Text":"e3 c2","NestedCollection":[{"Text":"e3 c2 c1"},{"Text":"e3 c2 c2"}],"NestedOptionalReference":{"Text":"e3 c2 nor"},"NestedRequiredReference":{"Text":"e3 c2 nrr"}}]', +N'{"Number":7,"Text":"e3 or","NestedCollection":[{"Text":"e3 or c1"},{"Text":"e3 or c2"}],"NestedOptionalReference":{"Text":"e3 or nor"},"NestedRequiredReference":{"Text":"e3 or nrr"}}', +N'{"Number":7,"Text":"e3 rr","NestedCollection":[{"Text":"e3 rr c1"},{"Text":"e3 rr c2"}],"NestedOptionalReference":{"Text":"e3 rr nor"},"NestedRequiredReference":{"Text":"e3 rr nrr"}}', +3, +N'e3') +"""); + + // null scalar on top level + await context.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO [Entities] ([Collection], [OptionalReference], [RequiredReference], [Id], [Name]) +VALUES ( +N'[{"Number":null,"Text":"e4 c1","NestedCollection":[{"Text":"e4 c1 c1"},{"Text":"e4 c1 c2"}],"NestedOptionalReference":{"Text":"e4 c1 nor"},"NestedRequiredReference":{"Text":"e4 c1 nrr"}},{"Number":null,"Text":"e4 c2","NestedCollection":[{"Text":"e4 c2 c1"},{"Text":"e4 c2 c2"}],"NestedOptionalReference":{"Text":"e4 c2 nor"},"NestedRequiredReference":{"Text":"e4 c2 nrr"}}]', +N'{"Number":null,"Text":"e4 or","NestedCollection":[{"Text":"e4 or c1"},{"Text":"e4 or c2"}],"NestedOptionalReference":{"Text":"e4 or nor"},"NestedRequiredReference":{"Text":"e4 or nrr"}}', +N'{"Number":null,"Text":"e4 rr","NestedCollection":[{"Text":"e4 rr c1"},{"Text":"e4 rr c2"}],"NestedOptionalReference":{"Text":"e4 rr nor"},"NestedRequiredReference":{"Text":"e4 rr nrr"}}', +4, +N'e4') +"""); + + // missing required navigation + await context.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO [Entities] ([Collection], [OptionalReference], [RequiredReference], [Id], [Name]) +VALUES ( +N'[{"Number":7,"Text":"e5 c1","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e5 c1 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e5 c1 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e5 c1 nor"}},{"Number":7,"Text":"e5 c2","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e5 c2 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e5 c2 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e5 c2 nor"}}]', +N'{"Number":7,"Text":"e5 or","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e5 or c1"},{"DoB":"2000-01-01T00:00:00","Text":"e5 or c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e5 or nor"}}', +N'{"Number":7,"Text":"e5 rr","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e5 rr c1"},{"DoB":"2000-01-01T00:00:00","Text":"e5 rr c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e5 rr nor"}}', +5, +N'e5') +"""); + + // null required navigation + await context.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO [Entities] ([Collection], [OptionalReference], [RequiredReference], [Id], [Name]) +VALUES ( +N'[{"Number":7,"Text":"e6 c1","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e6 c1 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e6 c1 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e6 c1 nor"},"NestedRequiredReference":null},{"Number":7,"Text":"e6 c2","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e6 c2 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e6 c2 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e6 c2 nor"},"NestedRequiredReference":null}]', +N'{"Number":7,"Text":"e6 or","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e6 or c1"},{"DoB":"2000-01-01T00:00:00","Text":"e6 or c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e6 or nor"},"NestedRequiredReference":null}', +N'{"Number":7,"Text":"e6 rr","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e6 rr c1"},{"DoB":"2000-01-01T00:00:00","Text":"e6 rr c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e6 rr nor"},"NestedRequiredReference":null}', +6, +N'e6') +"""); + } + + protected override async Task Seed29219(DbContext ctx) { var entity1 = new MyEntity29219 diff --git a/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs index 79529ed9b1d..e3a82cc168d 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs @@ -2070,6 +2070,11 @@ public override Task Rename_sequence() public override Task Move_sequence() => AssertNotSupportedAsync(base.Move_sequence, SqliteStrings.SequencesNotSupported); + public override Task Multiop_rename_table_and_drop() + => AssertNotSupportedAsync( + base.Multiop_rename_table_and_drop, + SqliteStrings.InvalidMigrationOperation(nameof(DropPrimaryKeyOperation))); + [ConditionalFact] public override async Task Add_required_primitve_collection_to_existing_table() { diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/AdHocJsonQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/AdHocJsonQuerySqliteTest.cs index c0e0a9b94a4..04dbe7f77aa 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/AdHocJsonQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/AdHocJsonQuerySqliteTest.cs @@ -5,11 +5,76 @@ namespace Microsoft.EntityFrameworkCore.Query; #nullable disable -public class AdHocJsonQuerySqliteTest : AdHocJsonQueryTestBase +public class AdHocJsonQuerySqliteTest : AdHocJsonQueryRelationalTestBase { protected override ITestStoreFactory TestStoreFactory => SqliteTestStoreFactory.Instance; + protected override async Task Seed21006(Context21006 context) + { + await base.Seed21006(context); + + // missing scalar on top level + await context.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO "Entities" ("Collection", "OptionalReference", "RequiredReference", "Id", "Name") +VALUES ( +'[{"Text":"e2 c1","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e2 c1 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e2 c1 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 c1 nor"},"NestedRequiredReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 c1 nrr"}},{"Text":"e2 c2","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e2 c2 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e2 c2 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 c2 nor"},"NestedRequiredReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 c2 nrr"}}]', +'{"Text":"e2 or","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e2 or c1"},{"DoB":"2000-01-01T00:00:00","Text":"e2 or c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 or nor"},"NestedRequiredReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 or nrr"}}', +'{"Text":"e2 rr","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e2 rr c1"},{"DoB":"2000-01-01T00:00:00","Text":"e2 rr c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 rr nor"},"NestedRequiredReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 rr nrr"}}', +2, +'e2') +"""); + + // missing scalar on nested level + await context.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO "Entities" ("Collection", "OptionalReference", "RequiredReference", "Id", "Name") +VALUES ( +'[{"Number":7,"Text":"e3 c1","NestedCollection":[{"Text":"e3 c1 c1"},{"Text":"e3 c1 c2"}],"NestedOptionalReference":{"Text":"e3 c1 nor"},"NestedRequiredReference":{"Text":"e3 c1 nrr"}},{"Number":7,"Text":"e3 c2","NestedCollection":[{"Text":"e3 c2 c1"},{"Text":"e3 c2 c2"}],"NestedOptionalReference":{"Text":"e3 c2 nor"},"NestedRequiredReference":{"Text":"e3 c2 nrr"}}]', +'{"Number":7,"Text":"e3 or","NestedCollection":[{"Text":"e3 or c1"},{"Text":"e3 or c2"}],"NestedOptionalReference":{"Text":"e3 or nor"},"NestedRequiredReference":{"Text":"e3 or nrr"}}', +'{"Number":7,"Text":"e3 rr","NestedCollection":[{"Text":"e3 rr c1"},{"Text":"e3 rr c2"}],"NestedOptionalReference":{"Text":"e3 rr nor"},"NestedRequiredReference":{"Text":"e3 rr nrr"}}', +3, +'e3') +"""); + + // null scalar on top level + await context.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO [Entities] ("Collection", "OptionalReference", "RequiredReference", "Id", "Name") +VALUES ( +'[{"Number":null,"Text":"e4 c1","NestedCollection":[{"Text":"e4 c1 c1"},{"Text":"e4 c1 c2"}],"NestedOptionalReference":{"Text":"e4 c1 nor"},"NestedRequiredReference":{"Text":"e4 c1 nrr"}},{"Number":null,"Text":"e4 c2","NestedCollection":[{"Text":"e4 c2 c1"},{"Text":"e4 c2 c2"}],"NestedOptionalReference":{"Text":"e4 c2 nor"},"NestedRequiredReference":{"Text":"e4 c2 nrr"}}]', +'{"Number":null,"Text":"e4 or","NestedCollection":[{"Text":"e4 or c1"},{"Text":"e4 or c2"}],"NestedOptionalReference":{"Text":"e4 or nor"},"NestedRequiredReference":{"Text":"e4 or nrr"}}', +'{"Number":null,"Text":"e4 rr","NestedCollection":[{"Text":"e4 rr c1"},{"Text":"e4 rr c2"}],"NestedOptionalReference":{"Text":"e4 rr nor"},"NestedRequiredReference":{"Text":"e4 rr nrr"}}', +4, +'e4') +"""); + + // missing required navigation + await context.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO "Entities" ("Collection", "OptionalReference", "RequiredReference", "Id", "Name") +VALUES ( +'[{"Number":7,"Text":"e5 c1","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e5 c1 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e5 c1 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e5 c1 nor"}},{"Number":7,"Text":"e5 c2","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e5 c2 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e5 c2 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e5 c2 nor"}}]', +'{"Number":7,"Text":"e5 or","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e5 or c1"},{"DoB":"2000-01-01T00:00:00","Text":"e5 or c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e5 or nor"}}', +'{"Number":7,"Text":"e5 rr","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e5 rr c1"},{"DoB":"2000-01-01T00:00:00","Text":"e5 rr c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e5 rr nor"}}', +5, +'e5') +"""); + + // null required navigation + await context.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO "Entities" ("Collection", "OptionalReference", "RequiredReference", "Id", "Name") +VALUES ( +'[{"Number":7,"Text":"e6 c1","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e6 c1 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e6 c1 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e6 c1 nor"},"NestedRequiredReference":null},{"Number":7,"Text":"e6 c2","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e6 c2 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e6 c2 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e6 c2 nor"},"NestedRequiredReference":null}]', +'{"Number":7,"Text":"e6 or","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e6 or c1"},{"DoB":"2000-01-01T00:00:00","Text":"e6 or c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e6 or nor"},"NestedRequiredReference":null}', +'{"Number":7,"Text":"e6 rr","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e6 rr c1"},{"DoB":"2000-01-01T00:00:00","Text":"e6 rr c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e6 rr nor"},"NestedRequiredReference":null}', +6, +'e6') +"""); + } + protected override async Task Seed29219(DbContext ctx) { var entity1 = new MyEntity29219