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