diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml index 4c98f0122..197dc166e 100644 --- a/.github/workflows/config.yml +++ b/.github/workflows/config.yml @@ -3,7 +3,7 @@ name: SingleStore EntityFramework Core on: [push] env: - DOTNET_VERSION: 7.0.404 + DOTNET_VERSION: 8.0.405 LICENSE_KEY: ${{ secrets.LICENSE_KEY }} SQL_USER_PASSWORD: ${{ secrets.SQL_USER_PASSWORD }} S2MS_API_KEY: ${{ secrets.S2MS_API_KEY }} @@ -15,7 +15,6 @@ jobs: fail-fast: false matrix: singlestore_image: - - singlestore/cluster-in-a-box:alma-8.1.30-e0a67e68e5-4.0.16-1.17.6 - singlestore/cluster-in-a-box:alma-8.5.6-b51bc5471a-4.0.17-1.17.8 - singlestore/cluster-in-a-box:alma-8.7.12-483e5f8acb-4.1.0-1.17.15 func_test_script: @@ -75,13 +74,13 @@ jobs: run: dotnet build SingleStore.EFCore.sln -c Release - name: Run Unit Tests - run: dotnet test test/EFCore.SingleStore.Tests -f net7.0 -c Release --no-build + run: dotnet test test/EFCore.SingleStore.Tests -f net8.0 -c Release --no-build - name: Rebuild migrations run: pwsh ./test/EFCore.SingleStore.IntegrationTests/scripts/rebuild.ps1 - name: Run Integration Tests - run: dotnet test test/EFCore.SingleStore.IntegrationTests -f net7.0 -c Release --no-build + run: dotnet test test/EFCore.SingleStore.IntegrationTests -f net8.0 -c Release --no-build - name: Run Functional Tests ${{ matrix.singlestore_image }} - ${{ matrix.func_test_script }} run: ${{ matrix.func_test_script }} @@ -121,13 +120,13 @@ jobs: run: dotnet build SingleStore.EFCore.sln -c Release - name: Run EFCore.SingleStore.Tests - run: .\.github\workflows\test_setup\run-test-windows.ps1 -test_block EFCore.SingleStore.Tests -target_framework net7.0 + run: .\.github\workflows\test_setup\run-test-windows.ps1 -test_block EFCore.SingleStore.Tests -target_framework net8.0 - name: Rebuild migrations run: .\test\EFCore.SingleStore.IntegrationTests\scripts\rebuild.ps1 - name: Run EFCore.SingleStore.IntegrationTests - run: .\.github\workflows\test_setup\run-test-windows.ps1 -test_block EFCore.SingleStore.IntegrationTests -target_framework net7.0 + run: .\.github\workflows\test_setup\run-test-windows.ps1 -test_block EFCore.SingleStore.IntegrationTests -target_framework net8.0 - name: Run Functional Test Block run: ${{ matrix.func_test_block_path }} diff --git a/.github/workflows/test_setup/config.json b/.github/workflows/test_setup/config.json index 965781fff..e1fc6d4f2 100644 --- a/.github/workflows/test_setup/config.json +++ b/.github/workflows/test_setup/config.json @@ -1,6 +1,6 @@ { "Data": { - "ConnectionString": "server=SINGLESTORE_HOST;user id=SQL_USER_NAME;password=SQL_USER_PASSWORD;port=3306;database=singlestoretest", + "ConnectionString": "server=SINGLESTORE_HOST;user id=SQL_USER_NAME;password=SQL_USER_PASSWORD;port=3306;database=singlestoretest;sslmode=None", "ServerVersion": "auto", "CommandTimeout": "600" } diff --git a/.github/workflows/test_setup/run-functional-tests1.ps1 b/.github/workflows/test_setup/run-functional-tests1.ps1 index 6f4022507..712939ef1 100644 --- a/.github/workflows/test_setup/run-functional-tests1.ps1 +++ b/.github/workflows/test_setup/run-functional-tests1.ps1 @@ -1,124 +1,124 @@ cd test\EFCore.SingleStore.FunctionalTests\ -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.ComplexNavigationsQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.ComplexNavigationsQuerySingleStoreTest.' $TOTAL_FAILURES = ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.Ef6GroupBySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.Ef6GroupBySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.CompositeKeysQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.CompositeKeysQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.CompositeKeysSplitQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.CompositeKeysSplitQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.DateOnlyQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.DateOnlyQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.EscapesSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.EscapesSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.EntitySplittingQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.EntitySplittingQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.FromSqlQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.FromSqlQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.FieldsOnlyLoadSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.FieldsOnlyLoadSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.FiltersInheritanceQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.FiltersInheritanceQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.FromSqlSprocQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.FromSqlSprocQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.FunkyDataQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.FunkyDataQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.GearsOfWarFromSqlQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.GearsOfWarFromSqlQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.GearsOfWarQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.GearsOfWarQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.IncludeOneToOneSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.IncludeOneToOneSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.InheritanceQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.InheritanceQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.ComplexNavigationsCollectionsQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.ComplexNavigationsCollectionsQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonMicrosoftPocoChangeTrackingTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonMicrosoftPocoChangeTrackingTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.BoolIndexingOptimizationDisabledSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.BoolIndexingOptimizationDisabledSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonMicrosoftDomChangeTrackingTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonMicrosoftDomChangeTrackingTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonMicrosoftDomQueryTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonMicrosoftDomQueryTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.InheritanceRelationshipsQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.InheritanceRelationshipsQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.ComplexNavigationsCollectionsSharedTypeQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.ComplexNavigationsCollectionsSharedTypeQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.ComplexNavigationsCollectionsSplitQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.ComplexNavigationsCollectionsSplitQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TPTFiltersInheritanceQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.TPTFiltersInheritanceQuerySingleStoreTest.' $TOTAL_FAILURES = ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TPTGearsOfWarQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.TPTGearsOfWarQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TPTInheritanceQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.TPTInheritanceQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TPTRelationshipsQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.TPTRelationshipsQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.WarningsSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.WarningsSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.ConcurrencyDetectorDisabledSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.ConcurrencyDetectorDisabledSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.CompositeKeyEndToEndSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.CompositeKeyEndToEndSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.CommandInterceptionSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.CommandInterceptionSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.BuiltInDataTypesSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.BuiltInDataTypesSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.ConnectionSettingsSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.ConnectionSettingsSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~ConnectionInterceptionSingleStoreTest' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~ConnectionInterceptionSingleStoreTest' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.ConferencePlannerSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.ConferencePlannerSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.ConcurrencyDetectorEnabledSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.ConcurrencyDetectorEnabledSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.DataAnnotationSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.DataAnnotationSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.CustomConvertersSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.CustomConvertersSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.ConvertToProviderTypesSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.ConvertToProviderTypesSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.ConnectionSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.ConnectionSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.EntitySplittingSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.EntitySplittingSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NonSharedModelUpdatesSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NonSharedModelUpdatesSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.DesignTimeSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.DesignTimeSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.DefaultValuesTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.DefaultValuesTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.DatabindingSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.DatabindingSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~FindSingleStoreTest' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~FindSingleStoreTest' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.FieldMappingSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.FieldMappingSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.ExistingConnectionSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.ExistingConnectionSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.GraphUpdatesSingleStoreTestBase.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.GraphUpdatesSingleStoreTestBase.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.GraphUpdatesSingleStoreClientNoActionTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.GraphUpdatesSingleStoreClientNoActionTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.GraphUpdatesSingleStoreClientCascadeTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.GraphUpdatesSingleStoreClientCascadeTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.FullInfrastructureMigrationsTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.FullInfrastructureMigrationsTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.LoggingSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.LoggingSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.LoadSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.LoadSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.LazyLoadProxySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.LazyLoadProxySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.KeysWithConvertersSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.KeysWithConvertersSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.ManyToManyLoadSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.ManyToManyLoadSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.ManyToManyTrackingSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.ManyToManyTrackingSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.MaterializationInterceptionSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.MaterializationInterceptionSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) diff --git a/.github/workflows/test_setup/run-functional-tests2.ps1 b/.github/workflows/test_setup/run-functional-tests2.ps1 index 14200d659..ac9cf1cc3 100644 --- a/.github/workflows/test_setup/run-functional-tests2.ps1 +++ b/.github/workflows/test_setup/run-functional-tests2.ps1 @@ -1,122 +1,122 @@ cd test\EFCore.SingleStore.FunctionalTests\ -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.ComplexNavigationsCollectionsSplitSharedTypeQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.ComplexNavigationsCollectionsSplitSharedTypeQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.ComplexNavigationsSharedTypeQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.ComplexNavigationsSharedTypeQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonMicrosoftPocoQueryTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonMicrosoftPocoQueryTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonMicrosoftStringChangeTrackingTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonMicrosoftStringChangeTrackingTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonMicrosoftStringQueryTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonMicrosoftStringQueryTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonNewtonsoftDomChangeTrackingTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonNewtonsoftDomChangeTrackingTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonNewtonsoftDomQueryTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonNewtonsoftDomQueryTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonNewtonsoftPocoChangeTrackingTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonNewtonsoftPocoChangeTrackingTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonNewtonsoftPocoQueryTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonNewtonsoftPocoQueryTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonNewtonsoftStringChangeTrackingTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonNewtonsoftStringChangeTrackingTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonNewtonsoftStringQueryTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonNewtonsoftStringQueryTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.ManyToManyHeterogeneousQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.ManyToManyHeterogeneousQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.MappingQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.MappingQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.MatchQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.MatchQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindAsTrackingQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindAsTrackingQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindAsNoTrackingQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindAsNoTrackingQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindAggregateQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindAggregateQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindAggregateOperatorsQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindAggregateOperatorsQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindDbFunctionsQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindDbFunctionsQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindCompiledQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindCompiledQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindChangeTrackingQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindChangeTrackingQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindGroupByQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindGroupByQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindFunctionsQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindFunctionsQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindEFPropertyIncludeQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindEFPropertyIncludeQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindKeylessEntitiesQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindKeylessEntitiesQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindJoinQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindJoinQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindIncludeQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindIncludeQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindIncludeNoTrackingQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindIncludeNoTrackingQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindQueryFiltersQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindQueryFiltersQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindMiscellaneousQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindMiscellaneousQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindSelectQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindSelectQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindSetOperationsQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindSetOperationsQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindSplitIncludeNoTrackingQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindSplitIncludeNoTrackingQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindSplitIncludeQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindSplitIncludeQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindSqlQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindSqlQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindStringIncludeQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindStringIncludeQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindWhereQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindWhereQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NullKeysSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NullKeysSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NullSemanticsQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NullSemanticsQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.OwnedEntityQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.OwnedEntityQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.OwnedQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.OwnedQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.QueryFilterFuncletizationSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.QueryFilterFuncletizationSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindNavigationsQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindNavigationsQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.SharedTypeQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.SharedTypeQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.QueryNoClientEvalSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.QueryNoClientEvalSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.SimpleQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.SimpleQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.SqlExecutorSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.SqlExecutorSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.ToSqlQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.ToSqlQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TPCGearsOfWarQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.TPCGearsOfWarQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TPCFiltersInheritanceQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.TPCFiltersInheritanceQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TPCInheritanceQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.TPCInheritanceQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TPCManyToManyNoTrackingQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.TPCManyToManyNoTrackingQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TPCManyToManyQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.TPCManyToManyQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TPCRelationshipsQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.TPCRelationshipsQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindBulkUpdatesSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindBulkUpdatesSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TPCFiltersInheritanceBulkUpdatesSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.TPCFiltersInheritanceBulkUpdatesSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TPCInheritanceBulkUpdatesSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.TPCInheritanceBulkUpdatesSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TPTFiltersInheritanceBulkUpdatesSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.TPTFiltersInheritanceBulkUpdatesSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TPTInheritanceBulkUpdatesSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.TPTInheritanceBulkUpdatesSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) cd ..\..\ diff --git a/.github/workflows/test_setup/run-functional-tests3.ps1 b/.github/workflows/test_setup/run-functional-tests3.ps1 index db2d3f03b..e46f3c35c 100644 --- a/.github/workflows/test_setup/run-functional-tests3.ps1 +++ b/.github/workflows/test_setup/run-functional-tests3.ps1 @@ -1,88 +1,78 @@ cd test\EFCore.SingleStore.FunctionalTests\ -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.MigrationsSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.MigrationsSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.MigrationsInfrastructureSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.MigrationsInfrastructureSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.OptimisticConcurrencySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.OptimisticConcurrencySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NotificationEntitiesSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NotificationEntitiesSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindQueryTaggingQuerySingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindQueryTaggingQuerySingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.MusicStoreSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.MusicStoreSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.PropertyValuesSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.PropertyValuesSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.OverzealousInitializationSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.OverzealousInitializationSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.SingleStoreDatabaseModelFactoryTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.SingleStoreDatabaseModelFactoryTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.StoreValueGenerationSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.StoreValueGenerationSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.SingleStoreTypeMappingTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.SingleStoreTypeMappingTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.StoredProcedureUpdateSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.StoredProcedureUpdateSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.SingleStoreJsonMicrosoftTypeMappingTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.SingleStoreJsonMicrosoftTypeMappingTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.SingleStoreJsonNewtonsoftTypeMappingTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.SingleStoreJsonNewtonsoftTypeMappingTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.SingleStoreUpdateSqlGeneratorTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.SingleStoreUpdateSqlGeneratorTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~SaveChangesInterceptionSingleStoreTest' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~SaveChangesInterceptionSingleStoreTest' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.SeedingSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.SeedingSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.SerializationSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.SerializationSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.SingleStoreMigrationsSqlGeneratorTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.SingleStoreMigrationsSqlGeneratorTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.SingleStoreComplianceTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.SingleStoreComplianceTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.SingleStoreApiConsistencyTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.SingleStoreApiConsistencyTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.StoreGeneratedSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.StoreGeneratedSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.StoreGeneratedFixupSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.StoreGeneratedFixupSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.SpatialSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.SpatialSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.SingleStoreServiceCollectionExtensionsTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.SingleStoreServiceCollectionExtensionsTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.SingleStoreNetTopologySuiteApiConsistencyTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.SingleStoreNetTopologySuiteApiConsistencyTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TableSplittingSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.TableSplittingSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TPTTableSplittingSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.TPTTableSplittingSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TransactionInterceptionSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.TransactionInterceptionSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TransactionSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.TransactionSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TwoDatabasesSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.TwoDatabasesSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.UpdatesSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.UpdatesSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.ValueConvertersEndToEndSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.ValueConvertersEndToEndSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.WithConstructorsSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.WithConstructorsSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.FiltersInheritanceBulkUpdatesSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.FiltersInheritanceBulkUpdatesSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.InheritanceBulkUpdatesSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.InheritanceBulkUpdatesSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NonSharedModelBulkUpdatesSingleStoreTest.' -$TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindBulkUpdatesSingleStoreTest.' -$TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TPCFiltersInheritanceBulkUpdatesSingleStoreTest.' -$TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TPCInheritanceBulkUpdatesSingleStoreTest.' -$TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TPTFiltersInheritanceBulkUpdatesSingleStoreTest.' -$TOTAL_FAILURES += ($LASTEXITCODE -ne 0) -dotnet.exe test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TPTInheritanceBulkUpdatesSingleStoreTest.' +dotnet.exe test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NonSharedModelBulkUpdatesSingleStoreTest.' $TOTAL_FAILURES += ($LASTEXITCODE -ne 0) cd ..\..\ diff --git a/.github/workflows/test_setup/run_functional_tests1.sh b/.github/workflows/test_setup/run_functional_tests1.sh index e625f5002..f4e5b9c34 100755 --- a/.github/workflows/test_setup/run_functional_tests1.sh +++ b/.github/workflows/test_setup/run_functional_tests1.sh @@ -1,168 +1,168 @@ cd test/EFCore.SingleStore.FunctionalTests/ -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TwoDatabasesSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.TwoDatabasesSingleStoreTest.' ((TOTAL_FAILURES = $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.CompositeKeysQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.CompositeKeysQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.ComplexNavigationsQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.ComplexNavigationsQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.Ef6GroupBySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.Ef6GroupBySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.CompositeKeysSplitQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.CompositeKeysSplitQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.DateOnlyQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.DateOnlyQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.EscapesSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.EscapesSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.EntitySplittingQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.EntitySplittingQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.FromSqlQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.FromSqlQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.FieldsOnlyLoadSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.FieldsOnlyLoadSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.FiltersInheritanceQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.FiltersInheritanceQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.FromSqlSprocQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.FromSqlSprocQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.FunkyDataQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.FunkyDataQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.GearsOfWarFromSqlQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.GearsOfWarFromSqlQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.GearsOfWarQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.GearsOfWarQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.IncludeOneToOneSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.IncludeOneToOneSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.InheritanceQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.InheritanceQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.ComplexNavigationsCollectionsQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.ComplexNavigationsCollectionsQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonMicrosoftPocoChangeTrackingTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonMicrosoftPocoChangeTrackingTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.BoolIndexingOptimizationDisabledSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.BoolIndexingOptimizationDisabledSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonMicrosoftDomChangeTrackingTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonMicrosoftDomChangeTrackingTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonMicrosoftDomQueryTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonMicrosoftDomQueryTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.InheritanceRelationshipsQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.InheritanceRelationshipsQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.ComplexNavigationsCollectionsSharedTypeQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.ComplexNavigationsCollectionsSharedTypeQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.ComplexNavigationsCollectionsSplitQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.ComplexNavigationsCollectionsSplitQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.ComplexNavigationsCollectionsSplitSharedTypeQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.ComplexNavigationsCollectionsSplitSharedTypeQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.ComplexNavigationsSharedTypeQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.ComplexNavigationsSharedTypeQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonMicrosoftPocoQueryTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonMicrosoftPocoQueryTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonMicrosoftStringChangeTrackingTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonMicrosoftStringChangeTrackingTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonMicrosoftStringQueryTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonMicrosoftStringQueryTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonNewtonsoftDomChangeTrackingTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonNewtonsoftDomChangeTrackingTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonNewtonsoftDomQueryTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonNewtonsoftDomQueryTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonNewtonsoftPocoChangeTrackingTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonNewtonsoftPocoChangeTrackingTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonNewtonsoftPocoQueryTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonNewtonsoftPocoQueryTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonNewtonsoftStringChangeTrackingTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonNewtonsoftStringChangeTrackingTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonNewtonsoftStringQueryTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.JsonNewtonsoftStringQueryTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.ManyToManyHeterogeneousQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.ManyToManyHeterogeneousQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.MappingQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.MappingQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.MatchQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.MatchQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindAsTrackingQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindAsTrackingQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindAsNoTrackingQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindAsNoTrackingQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindAggregateQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindAggregateQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindAggregateOperatorsQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindAggregateOperatorsQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindDbFunctionsQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindDbFunctionsQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindCompiledQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindCompiledQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindChangeTrackingQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindChangeTrackingQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindGroupByQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindGroupByQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindFunctionsQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindFunctionsQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindEFPropertyIncludeQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindEFPropertyIncludeQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindKeylessEntitiesQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindKeylessEntitiesQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindJoinQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindJoinQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindIncludeQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindIncludeQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindIncludeNoTrackingQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindIncludeNoTrackingQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindQueryFiltersQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindQueryFiltersQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindMiscellaneousQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindMiscellaneousQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindSelectQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindSelectQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindSetOperationsQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindSetOperationsQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindSplitIncludeNoTrackingQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindSplitIncludeNoTrackingQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindSplitIncludeQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindSplitIncludeQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindSqlQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindSqlQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindStringIncludeQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindStringIncludeQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindWhereQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindWhereQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NullKeysSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NullKeysSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NullSemanticsQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NullSemanticsQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.OwnedEntityQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.OwnedEntityQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.OwnedQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.OwnedQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.QueryFilterFuncletizationSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.QueryFilterFuncletizationSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindNavigationsQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindNavigationsQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.SharedTypeQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.SharedTypeQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.QueryNoClientEvalSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.QueryNoClientEvalSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.SimpleQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.SimpleQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.SqlExecutorSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.SqlExecutorSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.ToSqlQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.ToSqlQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.WithConstructorsSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.WithConstructorsSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.FiltersInheritanceBulkUpdatesSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.FiltersInheritanceBulkUpdatesSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.InheritanceBulkUpdatesSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.InheritanceBulkUpdatesSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NonSharedModelBulkUpdatesSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NonSharedModelBulkUpdatesSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindBulkUpdatesSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindBulkUpdatesSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TPCFiltersInheritanceBulkUpdatesSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.TPCFiltersInheritanceBulkUpdatesSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TPCInheritanceBulkUpdatesSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.TPCInheritanceBulkUpdatesSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TPTFiltersInheritanceBulkUpdatesSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.TPTFiltersInheritanceBulkUpdatesSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TPTInheritanceBulkUpdatesSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.TPTInheritanceBulkUpdatesSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) cd ../../ diff --git a/.github/workflows/test_setup/run_functional_tests2.sh b/.github/workflows/test_setup/run_functional_tests2.sh index 1ed38cfde..e1135ee75 100755 --- a/.github/workflows/test_setup/run_functional_tests2.sh +++ b/.github/workflows/test_setup/run_functional_tests2.sh @@ -1,152 +1,152 @@ cd test/EFCore.SingleStore.FunctionalTests/ -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.FullInfrastructureMigrationsTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.FullInfrastructureMigrationsTest.' ((TOTAL_FAILURES = $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TPCGearsOfWarQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.TPCGearsOfWarQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TPCFiltersInheritanceQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.TPCFiltersInheritanceQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TPCInheritanceQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.TPCInheritanceQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TPCManyToManyNoTrackingQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.TPCManyToManyNoTrackingQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TPCManyToManyQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.TPCManyToManyQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TPCRelationshipsQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.TPCRelationshipsQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TPTFiltersInheritanceQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.TPTFiltersInheritanceQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TPTGearsOfWarQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.TPTGearsOfWarQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TPTInheritanceQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.TPTInheritanceQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TPTRelationshipsQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.TPTRelationshipsQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.WarningsSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.WarningsSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.ConcurrencyDetectorDisabledSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.ConcurrencyDetectorDisabledSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.CompositeKeyEndToEndSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.CompositeKeyEndToEndSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.CommandInterceptionSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.CommandInterceptionSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.BuiltInDataTypesSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.BuiltInDataTypesSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.ConnectionSettingsSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.ConnectionSettingsSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~ConnectionInterceptionSingleStoreTest' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~ConnectionInterceptionSingleStoreTest' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.ConferencePlannerSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.ConferencePlannerSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.ConcurrencyDetectorEnabledSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.ConcurrencyDetectorEnabledSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.DataAnnotationSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.DataAnnotationSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.CustomConvertersSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.CustomConvertersSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.ConvertToProviderTypesSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.ConvertToProviderTypesSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.ConnectionSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.ConnectionSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.EntitySplittingSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.EntitySplittingSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NonSharedModelUpdatesSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NonSharedModelUpdatesSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.DesignTimeSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.DesignTimeSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.DefaultValuesTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.DefaultValuesTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.DatabindingSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.DatabindingSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~FindSingleStoreTest' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~FindSingleStoreTest' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.FieldMappingSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.FieldMappingSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.ExistingConnectionSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.ExistingConnectionSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.GraphUpdatesSingleStoreTestBase.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.GraphUpdatesSingleStoreTestBase.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.GraphUpdatesSingleStoreClientNoActionTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.GraphUpdatesSingleStoreClientNoActionTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.GraphUpdatesSingleStoreClientCascadeTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.GraphUpdatesSingleStoreClientCascadeTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.LoggingSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.LoggingSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.LoadSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.LoadSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.LazyLoadProxySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.LazyLoadProxySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.KeysWithConvertersSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.KeysWithConvertersSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.ManyToManyLoadSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.ManyToManyLoadSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.ManyToManyTrackingSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.ManyToManyTrackingSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.MaterializationInterceptionSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.MaterializationInterceptionSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.MigrationsSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.MigrationsSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.MigrationsInfrastructureSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.MigrationsInfrastructureSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.OptimisticConcurrencySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.OptimisticConcurrencySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NotificationEntitiesSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NotificationEntitiesSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindQueryTaggingQuerySingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.NorthwindQueryTaggingQuerySingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.MusicStoreSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.MusicStoreSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.PropertyValuesSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.PropertyValuesSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.OverzealousInitializationSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.OverzealousInitializationSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.SingleStoreDatabaseModelFactoryTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.SingleStoreDatabaseModelFactoryTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.StoreValueGenerationSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.StoreValueGenerationSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.SingleStoreTypeMappingTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.SingleStoreTypeMappingTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.StoredProcedureUpdateSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.StoredProcedureUpdateSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.SingleStoreJsonMicrosoftTypeMappingTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.SingleStoreJsonMicrosoftTypeMappingTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.SingleStoreJsonNewtonsoftTypeMappingTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.SingleStoreJsonNewtonsoftTypeMappingTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.SingleStoreUpdateSqlGeneratorTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.SingleStoreUpdateSqlGeneratorTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~SaveChangesInterceptionSingleStoreTest' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~SaveChangesInterceptionSingleStoreTest' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.SeedingSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.SeedingSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.SerializationSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.SerializationSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.SingleStoreMigrationsSqlGeneratorTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.SingleStoreMigrationsSqlGeneratorTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.SingleStoreComplianceTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.SingleStoreComplianceTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.SingleStoreApiConsistencyTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.SingleStoreApiConsistencyTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.StoreGeneratedSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.StoreGeneratedSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.StoreGeneratedFixupSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.StoreGeneratedFixupSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.SpatialSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.SpatialSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.SingleStoreServiceCollectionExtensionsTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.SingleStoreServiceCollectionExtensionsTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.SingleStoreNetTopologySuiteApiConsistencyTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.SingleStoreNetTopologySuiteApiConsistencyTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TableSplittingSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.TableSplittingSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TPTTableSplittingSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.TPTTableSplittingSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TransactionInterceptionSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.TransactionInterceptionSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.TransactionSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.TransactionSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.ValueConvertersEndToEndSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.ValueConvertersEndToEndSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) -dotnet test -f net7.0 -c Release --no-build --filter 'FullyQualifiedName~.UpdatesSingleStoreTest.' +dotnet test -f net8.0 -c Release --no-build --filter 'FullyQualifiedName~.UpdatesSingleStoreTest.' ((TOTAL_FAILURES += $? != 0)) cd ../../ diff --git a/.github/workflows/test_setup/s2ms_cluster.py b/.github/workflows/test_setup/s2ms_cluster.py index 9cbc9c2cd..88320c6e0 100644 --- a/.github/workflows/test_setup/s2ms_cluster.py +++ b/.github/workflows/test_setup/s2ms_cluster.py @@ -10,7 +10,7 @@ WORKSPACE_GROUP_BASE_NAME = "ef-core-provider-ci-test-cluster" WORKSPACE_NAME = "tests" -AWS_US_WEST_REGION = "1c1de314-2cc0-4c74-bd54-5047ff90842e" + AUTO_TERMINATE_MINUTES = 150 WORKSPACE_ENDPOINT_FILE = "WORKSPACE_ENDPOINT_FILE" WORKSPACE_GROUP_ID_FILE = "WORKSPACE_GROUP_ID_FILE" @@ -28,11 +28,16 @@ def retry(func): def create_workspace(workspace_manager): + for reg in workspace_manager.regions: + if 'US' in reg.name: + region = reg + break + w_group_name = WORKSPACE_GROUP_BASE_NAME + "-" + uuid.uuid4().hex def create_workspace_group(): return workspace_manager.create_workspace_group( name=w_group_name, - region=AWS_US_WEST_REGION, + region=region.id, firewall_ranges=["0.0.0.0/0"], admin_password=SQL_USER_PASSWORD, expires_at="150m" diff --git a/Dependencies.targets b/Dependencies.targets index ca97b0212..110ba5897 100644 --- a/Dependencies.targets +++ b/Dependencies.targets @@ -1,6 +1,6 @@ - 7.* + [8.0.0,8.0.999] @@ -9,40 +9,44 @@ - + - - + + - - - + + + - - - - - - - - - - - - - + + + + + + + + + + + + + - + + + + - - - + - + + + + \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props index 9866f2f48..7984d8a57 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -12,7 +12,6 @@ singlestore_logo.png true latest - portable true MIT https://github.com/memsql/SingleStore.EntityFrameworkCore @@ -21,10 +20,15 @@ - net6.0;net7.0 - net7.0 - net6.0 - net7.0 + portable + + + + net8.0 + net8.0 + net8.0 + net8.0 + net8.0 diff --git a/README.md b/README.md index 2e126b676..3eff634d7 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ Milestone | Status | Release Date ----------|----------------------|------------- +8.0.0| released | July 2025 7.0.1| released | March 2025 7.0.0| released | November 2024 6.0.2| general availability | April 2024 @@ -16,7 +17,7 @@ Milestone | Status | Release Date Ensure that your `.csproj` file contains the following reference: ```xml - + ``` ### 2. Services Configuration @@ -45,7 +46,7 @@ public class Startup } ``` -View our [Configuration Options Wiki Page](https://github.com/PomeloFoundation/EntityFrameworkCore.MySql/wiki/Configuration-Options) for a list of common options. +View our [Configuration Options Wiki Page](https://github.com/memsql/SingleStore.EntityFrameworkCore/wiki/Configuration-Options) for a list of common options. ### 3. Sample Application diff --git a/Version.props b/Version.props index c8aee9ac8..9ba606ba6 100644 --- a/Version.props +++ b/Version.props @@ -12,7 +12,7 @@ - "rtm" - EF Core release independent, code quality production ready, major release - "servicing" - EF Core release independent, code quality production ready, mainly bugfixes --> - 7.0.1 + 8.0.0 rtm @@ -73,4 +68,8 @@ + + + + diff --git a/src/EFCore.SingleStore.Json.Microsoft/Query/Internal/SingleStoreJsonMicrosoftDomTranslator.cs b/src/EFCore.SingleStore.Json.Microsoft/Query/Internal/SingleStoreJsonMicrosoftDomTranslator.cs index 9a3140041..67d76856d 100644 --- a/src/EFCore.SingleStore.Json.Microsoft/Query/Internal/SingleStoreJsonMicrosoftDomTranslator.cs +++ b/src/EFCore.SingleStore.Json.Microsoft/Query/Internal/SingleStoreJsonMicrosoftDomTranslator.cs @@ -186,7 +186,7 @@ private SqlExpression ApplyPathLocationTypeMapping(SqlExpression expression) sqlConstantExpression.TypeMapping is SingleStoreStringTypeMapping stringTypeMapping && !stringTypeMapping.IsUnquoted) { - pathLocation = sqlConstantExpression.ApplyTypeMapping(stringTypeMapping.Clone(true)); + pathLocation = sqlConstantExpression.ApplyTypeMapping(stringTypeMapping.Clone(unquoted: true)); } return pathLocation; diff --git a/src/EFCore.SingleStore.Json.Microsoft/Storage/Internal/SingleStoreJsonMicrosoftTypeMapping.cs b/src/EFCore.SingleStore.Json.Microsoft/Storage/Internal/SingleStoreJsonMicrosoftTypeMapping.cs index 3caddc3e5..dd3aca1fe 100644 --- a/src/EFCore.SingleStore.Json.Microsoft/Storage/Internal/SingleStoreJsonMicrosoftTypeMapping.cs +++ b/src/EFCore.SingleStore.Json.Microsoft/Storage/Internal/SingleStoreJsonMicrosoftTypeMapping.cs @@ -10,38 +10,53 @@ using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using SingleStoreConnector; -using EntityFrameworkCore.SingleStore.Infrastructure.Internal; using EntityFrameworkCore.SingleStore.Storage.Internal; namespace EntityFrameworkCore.SingleStore.Json.Microsoft.Storage.Internal { public class SingleStoreJsonMicrosoftTypeMapping : SingleStoreJsonTypeMapping { + public static new SingleStoreJsonMicrosoftTypeMapping Default { get; } = new("json", null, null, false, true); + // Called via reflection. // ReSharper disable once UnusedMember.Global public SingleStoreJsonMicrosoftTypeMapping( [NotNull] string storeType, [CanBeNull] ValueConverter valueConverter, [CanBeNull] ValueComparer valueComparer, - [NotNull] ISingleStoreOptions options) + bool noBackslashEscapes, + bool replaceLineBreaksWithCharFunction) : base( storeType, valueConverter, valueComparer, - options) + noBackslashEscapes, + replaceLineBreaksWithCharFunction) { } protected SingleStoreJsonMicrosoftTypeMapping( RelationalTypeMappingParameters parameters, SingleStoreDbType mySqlDbType, - ISingleStoreOptions options) - : base(parameters, mySqlDbType, options) + bool noBackslashEscapes, + bool replaceLineBreaksWithCharFunction) + : base( + parameters, + mySqlDbType, + noBackslashEscapes, + replaceLineBreaksWithCharFunction) { } protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new SingleStoreJsonMicrosoftTypeMapping(parameters, SingleStoreDbType, Options); + => new SingleStoreJsonMicrosoftTypeMapping(parameters, SingleStoreDbType, NoBackslashEscapes, ReplaceLineBreaksWithCharFunction); + + protected override RelationalTypeMapping Clone(bool? noBackslashEscapes = null, bool? replaceLineBreaksWithCharFunction = null) + => new SingleStoreJsonMicrosoftTypeMapping( + Parameters, + SingleStoreDbType, + noBackslashEscapes ?? NoBackslashEscapes, + replaceLineBreaksWithCharFunction ?? ReplaceLineBreaksWithCharFunction); public override Expression GenerateCodeLiteral(object value) => value switch diff --git a/src/EFCore.SingleStore.Json.Microsoft/Storage/Internal/SingleStoreJsonMicrosoftTypeMappingSourcePlugin.cs b/src/EFCore.SingleStore.Json.Microsoft/Storage/Internal/SingleStoreJsonMicrosoftTypeMappingSourcePlugin.cs index d0ca382f9..19c505562 100644 --- a/src/EFCore.SingleStore.Json.Microsoft/Storage/Internal/SingleStoreJsonMicrosoftTypeMappingSourcePlugin.cs +++ b/src/EFCore.SingleStore.Json.Microsoft/Storage/Internal/SingleStoreJsonMicrosoftTypeMappingSourcePlugin.cs @@ -41,7 +41,8 @@ protected override RelationalTypeMapping FindDomMapping(RelationalTypeMappingInf "json", GetValueConverter(clrType), GetValueComparer(clrType), - Options); + Options.NoBackslashEscapes, + Options.ReplaceLineBreaksWithCharFunction); } return null; diff --git a/src/EFCore.SingleStore.Json.Microsoft/Storage/ValueConversion/Internal/SingleStoreJsonMicrosoftJsonDocumentValueConverter.cs b/src/EFCore.SingleStore.Json.Microsoft/Storage/ValueConversion/Internal/SingleStoreJsonMicrosoftJsonDocumentValueConverter.cs index 59a85445d..1442a7197 100644 --- a/src/EFCore.SingleStore.Json.Microsoft/Storage/ValueConversion/Internal/SingleStoreJsonMicrosoftJsonDocumentValueConverter.cs +++ b/src/EFCore.SingleStore.Json.Microsoft/Storage/ValueConversion/Internal/SingleStoreJsonMicrosoftJsonDocumentValueConverter.cs @@ -19,7 +19,7 @@ public SingleStoreJsonMicrosoftJsonDocumentValueConverter() { } - private static string ConvertToProviderCore(JsonDocument v) + public static string ConvertToProviderCore(JsonDocument v) { using var stream = new MemoryStream(); using var writer = new Utf8JsonWriter(stream); @@ -28,7 +28,7 @@ private static string ConvertToProviderCore(JsonDocument v) return Encoding.UTF8.GetString(stream.ToArray()); } - private static JsonDocument ConvertFromProviderCore(string v) + public static JsonDocument ConvertFromProviderCore(string v) => JsonDocument.Parse(v); } } diff --git a/src/EFCore.SingleStore.Json.Microsoft/Storage/ValueConversion/Internal/SingleStoreJsonMicrosoftJsonElementValueConverter.cs b/src/EFCore.SingleStore.Json.Microsoft/Storage/ValueConversion/Internal/SingleStoreJsonMicrosoftJsonElementValueConverter.cs index 993441ae1..ec697b5c8 100644 --- a/src/EFCore.SingleStore.Json.Microsoft/Storage/ValueConversion/Internal/SingleStoreJsonMicrosoftJsonElementValueConverter.cs +++ b/src/EFCore.SingleStore.Json.Microsoft/Storage/ValueConversion/Internal/SingleStoreJsonMicrosoftJsonElementValueConverter.cs @@ -15,7 +15,7 @@ public SingleStoreJsonMicrosoftJsonElementValueConverter() { } - private static string ConvertToProviderCore(JsonElement v) + public static string ConvertToProviderCore(JsonElement v) { using var stream = new MemoryStream(); using var writer = new Utf8JsonWriter(stream); @@ -24,7 +24,7 @@ private static string ConvertToProviderCore(JsonElement v) return Encoding.UTF8.GetString(stream.ToArray()); } - private static JsonElement ConvertFromProviderCore(string v) + public static JsonElement ConvertFromProviderCore(string v) => JsonDocument.Parse(v).RootElement; } } diff --git a/src/EFCore.SingleStore.Json.Microsoft/Storage/ValueConversion/Internal/SingleStoreJsonMicrosoftPocoValueConverter.cs b/src/EFCore.SingleStore.Json.Microsoft/Storage/ValueConversion/Internal/SingleStoreJsonMicrosoftPocoValueConverter.cs index 841e86ecd..bc1b31174 100644 --- a/src/EFCore.SingleStore.Json.Microsoft/Storage/ValueConversion/Internal/SingleStoreJsonMicrosoftPocoValueConverter.cs +++ b/src/EFCore.SingleStore.Json.Microsoft/Storage/ValueConversion/Internal/SingleStoreJsonMicrosoftPocoValueConverter.cs @@ -17,10 +17,10 @@ public SingleStoreJsonMicrosoftPocoValueConverter() { } - private static string ConvertToProviderCore(T v) + public static string ConvertToProviderCore(T v) => JsonSerializer.Serialize(v); - private static T ConvertFromProviderCore(string v) + public static T ConvertFromProviderCore(string v) => JsonSerializer.Deserialize(v); } } diff --git a/src/EFCore.SingleStore.Json.Microsoft/Storage/ValueConversion/Internal/SingleStoreJsonMicrosoftStringValueConverter.cs b/src/EFCore.SingleStore.Json.Microsoft/Storage/ValueConversion/Internal/SingleStoreJsonMicrosoftStringValueConverter.cs index e2d508d0f..c51649d3a 100644 --- a/src/EFCore.SingleStore.Json.Microsoft/Storage/ValueConversion/Internal/SingleStoreJsonMicrosoftStringValueConverter.cs +++ b/src/EFCore.SingleStore.Json.Microsoft/Storage/ValueConversion/Internal/SingleStoreJsonMicrosoftStringValueConverter.cs @@ -19,10 +19,10 @@ public SingleStoreJsonMicrosoftStringValueConverter() { } - private static string ConvertToProviderCore(string v) + public static string ConvertToProviderCore(string v) => ProcessJsonString(v); - private static string ConvertFromProviderCore(string v) + public static string ConvertFromProviderCore(string v) => ProcessJsonString(v); internal static string ProcessJsonString(string v) diff --git a/src/EFCore.SingleStore.Json.Newtonsoft/EFCore.SingleStore.Json.Newtonsoft.csproj b/src/EFCore.SingleStore.Json.Newtonsoft/EFCore.SingleStore.Json.Newtonsoft.csproj index 1126bf382..95b867016 100644 --- a/src/EFCore.SingleStore.Json.Newtonsoft/EFCore.SingleStore.Json.Newtonsoft.csproj +++ b/src/EFCore.SingleStore.Json.Newtonsoft/EFCore.SingleStore.Json.Newtonsoft.csproj @@ -2,8 +2,7 @@ JSON support using Newtonsoft.Json (JSON.NET) for SingleStore provider for Entity Framework Core. - $(SingleStoreTargetFrameworks) - 3.6 + $(SingleStoreTargetFramework) EntityFrameworkCore.SingleStore.Json.Newtonsoft EntityFrameworkCore.SingleStore.Json.Newtonsoft true @@ -34,7 +33,7 @@ - + @@ -45,13 +44,6 @@ - - - - $(LocalSingleStoreConnectorRepository)\src\SingleStoreConnector\bin\Debug\$(TargetFramework)\SingleStoreConnector.dll - - - diff --git a/src/EFCore.SingleStore.Json.Newtonsoft/Query/Internal/SingleStoreJsonNewtonsoftDomTranslator.cs b/src/EFCore.SingleStore.Json.Newtonsoft/Query/Internal/SingleStoreJsonNewtonsoftDomTranslator.cs index 351eb8978..14bb923ed 100644 --- a/src/EFCore.SingleStore.Json.Newtonsoft/Query/Internal/SingleStoreJsonNewtonsoftDomTranslator.cs +++ b/src/EFCore.SingleStore.Json.Newtonsoft/Query/Internal/SingleStoreJsonNewtonsoftDomTranslator.cs @@ -182,7 +182,7 @@ private SqlExpression ApplyPathLocationTypeMapping(SqlExpression expression) sqlConstantExpression.TypeMapping is SingleStoreStringTypeMapping stringTypeMapping && !stringTypeMapping.IsUnquoted) { - pathLocation = sqlConstantExpression.ApplyTypeMapping(stringTypeMapping.Clone(true)); + pathLocation = sqlConstantExpression.ApplyTypeMapping(stringTypeMapping.Clone(unquoted: true)); } return pathLocation; diff --git a/src/EFCore.SingleStore.Json.Newtonsoft/Storage/Internal/SingleStoreJsonNewtonsoftTypeMapping.cs b/src/EFCore.SingleStore.Json.Newtonsoft/Storage/Internal/SingleStoreJsonNewtonsoftTypeMapping.cs index bdd5e84d1..4c3ff3f37 100644 --- a/src/EFCore.SingleStore.Json.Newtonsoft/Storage/Internal/SingleStoreJsonNewtonsoftTypeMapping.cs +++ b/src/EFCore.SingleStore.Json.Newtonsoft/Storage/Internal/SingleStoreJsonNewtonsoftTypeMapping.cs @@ -4,7 +4,6 @@ using System; using System.Linq.Expressions; -using System.Reflection; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Storage; @@ -12,38 +11,49 @@ using SingleStoreConnector; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using EntityFrameworkCore.SingleStore.Infrastructure.Internal; using EntityFrameworkCore.SingleStore.Storage.Internal; namespace EntityFrameworkCore.SingleStore.Json.Newtonsoft.Storage.Internal { public class SingleStoreJsonNewtonsoftTypeMapping : SingleStoreJsonTypeMapping { + public static new SingleStoreJsonNewtonsoftTypeMapping Default { get; } = new("json", null, null, false, true); + // Called via reflection. // ReSharper disable once UnusedMember.Global public SingleStoreJsonNewtonsoftTypeMapping( [NotNull] string storeType, [CanBeNull] ValueConverter valueConverter, [CanBeNull] ValueComparer valueComparer, - [NotNull] ISingleStoreOptions options) + bool noBackslashEscapes, + bool replaceLineBreaksWithCharFunction) : base( storeType, valueConverter, valueComparer, - options) + noBackslashEscapes, + replaceLineBreaksWithCharFunction) { } protected SingleStoreJsonNewtonsoftTypeMapping( RelationalTypeMappingParameters parameters, SingleStoreDbType mySqlDbType, - ISingleStoreOptions options) - : base(parameters, mySqlDbType, options) + bool noBackslashEscapes, + bool replaceLineBreaksWithCharFunction) + : base(parameters, mySqlDbType, noBackslashEscapes, replaceLineBreaksWithCharFunction) { } protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new SingleStoreJsonNewtonsoftTypeMapping(parameters, SingleStoreDbType, Options); + => new SingleStoreJsonNewtonsoftTypeMapping(parameters, SingleStoreDbType, NoBackslashEscapes, ReplaceLineBreaksWithCharFunction); + + protected override RelationalTypeMapping Clone(bool? noBackslashEscapes = null, bool? replaceLineBreaksWithCharFunction = null) + => new SingleStoreJsonNewtonsoftTypeMapping( + Parameters, + SingleStoreDbType, + noBackslashEscapes ?? NoBackslashEscapes, + replaceLineBreaksWithCharFunction ?? ReplaceLineBreaksWithCharFunction); public override Expression GenerateCodeLiteral(object value) => value switch diff --git a/src/EFCore.SingleStore.Json.Newtonsoft/Storage/Internal/SingleStoreJsonNewtonsoftTypeMappingSourcePlugin.cs b/src/EFCore.SingleStore.Json.Newtonsoft/Storage/Internal/SingleStoreJsonNewtonsoftTypeMappingSourcePlugin.cs index 1047ad243..1798e4202 100644 --- a/src/EFCore.SingleStore.Json.Newtonsoft/Storage/Internal/SingleStoreJsonNewtonsoftTypeMappingSourcePlugin.cs +++ b/src/EFCore.SingleStore.Json.Newtonsoft/Storage/Internal/SingleStoreJsonNewtonsoftTypeMappingSourcePlugin.cs @@ -39,7 +39,8 @@ protected override RelationalTypeMapping FindDomMapping(RelationalTypeMappingInf "json", GetValueConverter(clrType), GetValueComparer(clrType), - Options); + Options.NoBackslashEscapes, + Options.ReplaceLineBreaksWithCharFunction); } return null; diff --git a/src/EFCore.SingleStore.NTS/EFCore.SingleStore.NTS.csproj b/src/EFCore.SingleStore.NTS/EFCore.SingleStore.NTS.csproj index 1b9040b3b..671c80b57 100644 --- a/src/EFCore.SingleStore.NTS/EFCore.SingleStore.NTS.csproj +++ b/src/EFCore.SingleStore.NTS/EFCore.SingleStore.NTS.csproj @@ -2,8 +2,7 @@ NetTopologySuite support for SingleStore provider for Entity Framework Core. - $(SingleStoreTargetFrameworks) - 3.6 + $(SingleStoreTargetFramework) EntityFrameworkCore.SingleStore.NetTopologySuite EntityFrameworkCore.SingleStore true @@ -34,7 +33,7 @@ - + @@ -45,13 +44,6 @@ - - - - $(LocalSingleStoreConnectorRepository)\src\SingleStoreConnector\bin\Debug\$(TargetFramework)\SingleStoreConnector.dll - - - diff --git a/src/EFCore.SingleStore/Design/Internal/SingleStoreCSharpRuntimeAnnotationCodeGenerator.cs b/src/EFCore.SingleStore/Design/Internal/SingleStoreCSharpRuntimeAnnotationCodeGenerator.cs new file mode 100644 index 000000000..5f2a326fa --- /dev/null +++ b/src/EFCore.SingleStore/Design/Internal/SingleStoreCSharpRuntimeAnnotationCodeGenerator.cs @@ -0,0 +1,37 @@ +// Copyright (c) Pomelo Foundation. All rights reserved. +// Copyright (c) SingleStore Inc. All rights reserved. +// Licensed under the MIT. See LICENSE in the project root for license information. + +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Design.Internal; +using Microsoft.EntityFrameworkCore.Storage; +using EntityFrameworkCore.SingleStore.Storage.Internal; + +namespace EntityFrameworkCore.SingleStore.Design.Internal; + +public class SingleStoreCSharpRuntimeAnnotationCodeGenerator : RelationalCSharpRuntimeAnnotationCodeGenerator +{ + public SingleStoreCSharpRuntimeAnnotationCodeGenerator( + CSharpRuntimeAnnotationCodeGeneratorDependencies dependencies, + RelationalCSharpRuntimeAnnotationCodeGeneratorDependencies relationalDependencies) + : base(dependencies, relationalDependencies) + { + } + + public override bool Create( + CoreTypeMapping typeMapping, + CSharpRuntimeAnnotationCodeGeneratorParameters parameters, + ValueComparer valueComparer = null, + ValueComparer keyValueComparer = null, + ValueComparer providerValueComparer = null) + { + var result = base.Create(typeMapping, parameters, valueComparer, keyValueComparer, providerValueComparer); + + if (typeMapping is ISingleStoreCSharpRuntimeAnnotationTypeMappingCodeGenerator extension) + { + extension.Create(parameters, Dependencies); + } + + return result; + } +} diff --git a/src/EFCore.SingleStore/Design/Internal/SingleStoreDesignTimeServices.cs b/src/EFCore.SingleStore/Design/Internal/SingleStoreDesignTimeServices.cs index 3b6e56121..f22cc3098 100644 --- a/src/EFCore.SingleStore/Design/Internal/SingleStoreDesignTimeServices.cs +++ b/src/EFCore.SingleStore/Design/Internal/SingleStoreDesignTimeServices.cs @@ -4,6 +4,7 @@ using EntityFrameworkCore.SingleStore.Scaffolding.Internal; using Microsoft.EntityFrameworkCore.Design; +using Microsoft.EntityFrameworkCore.Design.Internal; using Microsoft.EntityFrameworkCore.Scaffolding; using Microsoft.Extensions.DependencyInjection; @@ -15,6 +16,7 @@ public virtual void ConfigureDesignTimeServices(IServiceCollection serviceCollec { serviceCollection.AddEntityFrameworkSingleStore(); new EntityFrameworkRelationalDesignServicesBuilder(serviceCollection) + .TryAdd() .TryAdd() .TryAdd() .TryAdd() diff --git a/src/EFCore.SingleStore/EFCore.SingleStore.csproj b/src/EFCore.SingleStore/EFCore.SingleStore.csproj index b14470998..3bf900f1b 100644 --- a/src/EFCore.SingleStore/EFCore.SingleStore.csproj +++ b/src/EFCore.SingleStore/EFCore.SingleStore.csproj @@ -2,8 +2,7 @@ SingleStore database provider for Entity Framework Core. - $(SingleStoreTargetFrameworks) - 3.6 + $(SingleStoreTargetFramework) EntityFrameworkCore.SingleStore EntityFrameworkCore.SingleStore README.md @@ -51,7 +50,7 @@ - + diff --git a/src/EFCore.SingleStore/Extensions/SingleStoreDatabaseFacadeExtensions.cs b/src/EFCore.SingleStore/Extensions/SingleStoreDatabaseFacadeExtensions.cs index fc57d88f1..1cfcd4fda 100644 --- a/src/EFCore.SingleStore/Extensions/SingleStoreDatabaseFacadeExtensions.cs +++ b/src/EFCore.SingleStore/Extensions/SingleStoreDatabaseFacadeExtensions.cs @@ -3,22 +3,26 @@ // Licensed under the MIT. See LICENSE in the project root for license information. using System; +using System.Data.Common; using System.Reflection; using EntityFrameworkCore.SingleStore.Infrastructure.Internal; using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage; +using EntityFrameworkCore.SingleStore.Storage.Internal; //ReSharper disable once CheckNamespace namespace Microsoft.EntityFrameworkCore { /// - /// MySQL specific extension methods for . + /// SingleStore specific extension methods for . /// public static class SingleStoreDatabaseFacadeExtensions { /// /// - /// Returns true if the database provider currently in use is the MySQL provider. + /// Returns true if the database provider currently in use is the SingleStore provider. /// /// /// This method can only be used after the has been configured because @@ -28,10 +32,38 @@ public static class SingleStoreDatabaseFacadeExtensions /// /// /// The facade from . - /// True if MySQL is being used; false otherwise. + /// True if SingleStore is being used; false otherwise. public static bool IsSingleStore([NotNull] this DatabaseFacade database) => database.ProviderName.Equals( typeof(SingleStoreOptionsExtension).GetTypeInfo().Assembly.GetName().Name, StringComparison.Ordinal); + + /// + /// Uses a for this as the underlying database provider. + /// + /// + /// + /// It may not be possible to change the data source if an existing connection is open. + /// + /// + /// See Connections and connection strings for more information and examples. + /// + /// + /// The for the context. + /// The data source. + public static void SetDbDataSource(this DatabaseFacade databaseFacade, DbDataSource dataSource) + => ((SingleStoreRelationalConnection)GetFacadeDependencies(databaseFacade).RelationalConnection).DbDataSource = dataSource; + + private static IRelationalDatabaseFacadeDependencies GetFacadeDependencies(DatabaseFacade databaseFacade) + { + var dependencies = ((IDatabaseFacadeDependenciesAccessor)databaseFacade).Dependencies; + + if (dependencies is IRelationalDatabaseFacadeDependencies relationalDependencies) + { + return relationalDependencies; + } + + throw new InvalidOperationException(RelationalStrings.RelationalNotInUse); + } } } diff --git a/src/EFCore.SingleStore/Extensions/SingleStoreDbContextOptionsBuilderExtensions.cs b/src/EFCore.SingleStore/Extensions/SingleStoreDbContextOptionsBuilderExtensions.cs index 2c40d8343..654ec91dd 100644 --- a/src/EFCore.SingleStore/Extensions/SingleStoreDbContextOptionsBuilderExtensions.cs +++ b/src/EFCore.SingleStore/Extensions/SingleStoreDbContextOptionsBuilderExtensions.cs @@ -49,8 +49,10 @@ public static DbContextOptionsBuilder UseSingleStore( optionsBuilder.AddInterceptors(new MatchInterceptor()); ServerVersion serverVersion = SingleStoreServerVersion.LatestSupportedServerVersion; + var extension = (SingleStoreOptionsExtension)GetOrCreateExtension(optionsBuilder) .WithServerVersion(serverVersion); + ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension); ConfigureWarnings(optionsBuilder); mySqlOptionsAction?.Invoke(new SingleStoreDbContextOptionsBuilder(optionsBuilder)); @@ -75,22 +77,6 @@ public static DbContextOptionsBuilder UseSingleStore( optionsBuilder.AddInterceptors(new MatchInterceptor()); - if (connectionString is not null) - { - var resolvedConnectionString = new NamedConnectionStringResolver(optionsBuilder.Options) - .ResolveConnectionString(connectionString); - - var programVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString(); - var csb = new SingleStoreConnectionStringBuilder(resolvedConnectionString) - { - AllowUserVariables = true, - UseAffectedRows = false, - ConnectionAttributes = $"_connector_name:SingleStore Entity Framework Core provider,_connector_version:{programVersion}", - }; - - connectionString = csb.ConnectionString; - } - ServerVersion serverVersion; try @@ -137,37 +123,55 @@ public static DbContextOptionsBuilder UseSingleStore( Check.NotNull(connection, nameof(connection)); optionsBuilder.AddInterceptors(new MatchInterceptor()); - var resolvedConnectionString = connection.ConnectionString is not null - ? new NamedConnectionStringResolver(optionsBuilder.Options) - .ResolveConnectionString(connection.ConnectionString) - : null; - - var csb = new SingleStoreConnectionStringBuilder(resolvedConnectionString); - var programVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString(); - csb.ConnectionAttributes = $"_connector_name:SingleStore Entity Framework Core provider,_connector_version:{programVersion}"; + ServerVersion serverVersion; - if (!csb.AllowUserVariables || - csb.UseAffectedRows) + try { - try - { - csb.AllowUserVariables = true; - csb.UseAffectedRows = false; - - connection.ConnectionString = csb.ConnectionString; - } - catch (Exception e) - { - throw new InvalidOperationException( - @"The connection string of a connection used by EntityFrameworkCore.SingleStore must contain ""AllowUserVariables=true;UseAffectedRows=false"".", - e); - } + serverVersion = ServerVersion.AutoDetect(connection.ConnectionString); + } + // There might occur different types of Exceptions while trying to AutoDetect() server version. + // This includes: SocketException (when we're unable to connect to any of the specified hosts), + // InvalidOperationException (unable to determine server version from version string), etc. + // In this case the latest supported server version will be used, therefore we catch any Exception. + catch (Exception) + { + serverVersion = SingleStoreServerVersion.LatestSupportedServerVersion; } + var extension = (SingleStoreOptionsExtension)GetOrCreateExtension(optionsBuilder) + .WithServerVersion(serverVersion) + .WithConnection(connection); + + ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension); + ConfigureWarnings(optionsBuilder); + mySqlOptionsAction?.Invoke(new SingleStoreDbContextOptionsBuilder(optionsBuilder)); + + return optionsBuilder; + } + + /// + /// Configures the context to connect to a MySQL compatible database. + /// + /// The builder being used to configure the context. + /// A which will be used to get database connections. + /// An optional action to allow additional MySQL specific configuration. + /// The options builder so that further configuration can be chained. + public static DbContextOptionsBuilder UseSingleStore( + this DbContextOptionsBuilder optionsBuilder, + DbDataSource dataSource, + Action mySqlOptionsAction = null) + { + Check.NotNull(optionsBuilder, nameof(optionsBuilder)); + Check.NotNull(dataSource, nameof(dataSource)); + + optionsBuilder.AddInterceptors(new MatchInterceptor()); ServerVersion serverVersion; try { + using var connection = dataSource.CreateConnection(); + connection.Open(); + serverVersion = ServerVersion.AutoDetect(connection.ConnectionString); } // There might occur different types of Exceptions while trying to AutoDetect() server version. @@ -181,7 +185,7 @@ public static DbContextOptionsBuilder UseSingleStore( var extension = (SingleStoreOptionsExtension)GetOrCreateExtension(optionsBuilder) .WithServerVersion(serverVersion) - .WithConnection(connection); + .WithDataSource(dataSource); ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension); ConfigureWarnings(optionsBuilder); @@ -248,6 +252,21 @@ public static DbContextOptionsBuilder UseSingleStore( => (DbContextOptionsBuilder)UseSingleStore( (DbContextOptionsBuilder)optionsBuilder, connection, mySqlOptionsAction); + /// + /// Configures the context to connect to a MySQL compatible database. + /// + /// The builder being used to configure the context. + /// A which will be used to get database connections. + /// An optional action to allow additional MySQL specific configuration. + /// The options builder so that further configuration can be chained. + public static DbContextOptionsBuilder UseSingleStore( + [NotNull] this DbContextOptionsBuilder optionsBuilder, + [NotNull] DbDataSource dataSource, + [CanBeNull] Action mySqlOptionsAction = null) + where TContext : DbContext + => (DbContextOptionsBuilder)UseSingleStore( + (DbContextOptionsBuilder)optionsBuilder, dataSource, mySqlOptionsAction); + private static SingleStoreOptionsExtension GetOrCreateExtension(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.Options.FindExtension() ?? new SingleStoreOptionsExtension(); diff --git a/src/EFCore.SingleStore/Extensions/SingleStoreDbFunctionsExtensions.cs b/src/EFCore.SingleStore/Extensions/SingleStoreDbFunctionsExtensions.cs index 4fbfef9d8..95ca81215 100644 --- a/src/EFCore.SingleStore/Extensions/SingleStoreDbFunctionsExtensions.cs +++ b/src/EFCore.SingleStore/Extensions/SingleStoreDbFunctionsExtensions.cs @@ -665,11 +665,11 @@ public static bool Like( /// The pattern against which Full Text search is performed /// true if there is a match. /// Throws when query switched to client-evaluation. - public static bool Match( + public static bool IsMatch( [CanBeNull] this DbFunctions _, [CanBeNull] string property, [CanBeNull] string pattern) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Match))); + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(IsMatch))); /// /// @@ -685,7 +685,47 @@ public static bool Match( /// The pattern against which Full Text search is performed /// true if there is a match. /// Throws when query switched to client-evaluation. - public static bool Match( + public static bool IsMatch( + [CanBeNull] this DbFunctions _, + [NotNull] string[] properties, + [CanBeNull] string pattern) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(IsMatch))); + + /// + /// + /// An implementation of the SQL MATCH operation for Full Text search. + /// + /// + /// The semantics of the comparison will depend on the database configuration. + /// In particular, it may be either case-sensitive or case-insensitive. + /// + /// + /// The DbFunctions instance. + /// The property of entity that is to be matched. + /// The pattern against which Full Text search is performed + /// The relevance value of the match. + /// Throws when query switched to client-evaluation. + public static double Match( + [CanBeNull] this DbFunctions _, + [CanBeNull] string property, + [CanBeNull] string pattern) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Match))); + + /// + /// + /// An implementation of the SQL MATCH operation for Full Text search. + /// + /// + /// The semantics of the comparison will depend on the database configuration. + /// In particular, it may be either case-sensitive or case-insensitive. + /// + /// + /// The DbFunctions instance. + /// The propertys of entity that is to be matched. + /// The pattern against which Full Text search is performed + /// The relevance value of the match. + /// Throws when query switched to client-evaluation. + public static double Match( [CanBeNull] this DbFunctions _, [NotNull] string[] properties, [CanBeNull] string pattern) diff --git a/src/EFCore.SingleStore/Extensions/SingleStorePropertyExtensions.cs b/src/EFCore.SingleStore/Extensions/SingleStorePropertyExtensions.cs index 2d075bad9..e50375d7e 100644 --- a/src/EFCore.SingleStore/Extensions/SingleStorePropertyExtensions.cs +++ b/src/EFCore.SingleStore/Extensions/SingleStorePropertyExtensions.cs @@ -88,7 +88,7 @@ internal static SingleStoreValueGenerationStrategy GetValueGenerationStrategy( var annotation = property.FindAnnotation(SingleStoreAnnotationNames.ValueGenerationStrategy); if (annotation?.Value is { } annotationValue && ObjectToEnumConverter.GetEnumValue(annotationValue) is { } enumValue - && StoreObjectIdentifier.Create(property.DeclaringEntityType, storeObject.StoreObjectType) == storeObject) + && StoreObjectIdentifier.Create(property.DeclaringType, storeObject.StoreObjectType) == storeObject) { return enumValue; } @@ -171,7 +171,7 @@ is StoreObjectIdentifier principal private static SingleStoreValueGenerationStrategy GetDefaultValueGenerationStrategy(IReadOnlyProperty property) { - var modelStrategy = property.DeclaringEntityType.Model.GetValueGenerationStrategy(); + var modelStrategy = property.DeclaringType.Model.GetValueGenerationStrategy(); if (modelStrategy == SingleStoreValueGenerationStrategy.IdentityColumn && IsCompatibleAutoIncrementColumn(property)) @@ -187,7 +187,7 @@ private static SingleStoreValueGenerationStrategy GetDefaultValueGenerationStrat in StoreObjectIdentifier storeObject, [CanBeNull] ITypeMappingSource typeMappingSource) { - var modelStrategy = property.DeclaringEntityType.Model.GetValueGenerationStrategy(); + var modelStrategy = property.DeclaringType.Model.GetValueGenerationStrategy(); return modelStrategy == SingleStoreValueGenerationStrategy.IdentityColumn && IsCompatibleAutoIncrementColumn(property, storeObject, typeMappingSource) @@ -323,7 +323,7 @@ public static void SetValueGenerationStrategy( { throw new ArgumentException( SingleStoreStrings.IdentityBadType( - property.Name, property.DeclaringEntityType.DisplayName(), propertyType.ShortDisplayName())); + property.Name, property.DeclaringType.DisplayName(), propertyType.ShortDisplayName())); } if (value == SingleStoreValueGenerationStrategy.ComputedColumn @@ -331,7 +331,7 @@ public static void SetValueGenerationStrategy( { throw new ArgumentException( SingleStoreStrings.ComputedBadType( - property.Name, property.DeclaringEntityType.DisplayName(), propertyType.ShortDisplayName())); + property.Name, property.DeclaringType.DisplayName(), propertyType.ShortDisplayName())); } return value; diff --git a/src/EFCore.SingleStore/Extensions/SingleStoreServiceCollectionExtensions.cs b/src/EFCore.SingleStore/Extensions/SingleStoreServiceCollectionExtensions.cs index 4130f6f44..7163cfc3a 100644 --- a/src/EFCore.SingleStore/Extensions/SingleStoreServiceCollectionExtensions.cs +++ b/src/EFCore.SingleStore/Extensions/SingleStoreServiceCollectionExtensions.cs @@ -28,7 +28,6 @@ using EntityFrameworkCore.SingleStore.Migrations; using EntityFrameworkCore.SingleStore.Query.ExpressionVisitors.Internal; using EntityFrameworkCore.SingleStore.Query.Internal; -using EntityFrameworkCore.SingleStore.Storage; // ReSharper disable once CheckNamespace namespace Microsoft.Extensions.DependencyInjection @@ -111,7 +110,7 @@ public static IServiceCollection AddEntityFrameworkSingleStore([NotNull] this IS .TryAdd() .TryAdd() .TryAdd() - .TryAdd(p => p.GetService()) + .TryAdd(p => p.GetRequiredService()) .TryAdd() .TryAdd() .TryAdd() @@ -126,15 +125,22 @@ public static IServiceCollection AddEntityFrameworkSingleStore([NotNull] this IS .TryAdd() .TryAdd() .TryAdd() - .TryAdd(p => p.GetService()) + .TryAdd(p => p.GetRequiredService()) //.TryAdd() .TryAdd() .TryAdd() + + // TODO: Injecting this service will make our original JSON implementations work, but interferes with EF Core 8's new + // primitive collections support. + // We will need to limit the preprocessor logic to only the relevant cases. + .TryAdd() + .TryAdd() .TryAdd() .TryAddProviderSpecificServices(m => m //.TryAddSingleton() .TryAddSingleton() + .TryAddSingleton() //.TryAddScoped() .TryAddScoped() .TryAddScoped()); diff --git a/src/EFCore.SingleStore/Infrastructure/Internal/ISingleStoreOptions.cs b/src/EFCore.SingleStore/Infrastructure/Internal/ISingleStoreOptions.cs index d6630dcd7..1b082ad44 100644 --- a/src/EFCore.SingleStore/Infrastructure/Internal/ISingleStoreOptions.cs +++ b/src/EFCore.SingleStore/Infrastructure/Internal/ISingleStoreOptions.cs @@ -2,6 +2,7 @@ // Copyright (c) SingleStore Inc. All rights reserved. // Licensed under the MIT. See LICENSE in the project root for license information. +using System.Data.Common; using Microsoft.EntityFrameworkCore; using EntityFrameworkCore.SingleStore.Storage.Internal; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -11,6 +12,12 @@ namespace EntityFrameworkCore.SingleStore.Infrastructure.Internal public interface ISingleStoreOptions : ISingletonOptions { SingleStoreConnectionSettings ConnectionSettings { get; } + + /// + /// If null, there might still be a `DbDataSource` in the ApplicationServiceProvider. + /// > + DbDataSource DataSource { get; } + ServerVersion ServerVersion { get; } CharSet DefaultCharSet { get; } CharSet NationalCharSet { get; } @@ -23,5 +30,6 @@ public interface ISingleStoreOptions : ISingletonOptions SingleStoreJsonChangeTrackingOptions JsonChangeTrackingOptions { get; } bool LimitKeyedOrIndexedStringColumnLength { get; } bool StringComparisonTranslations { get; } + bool PrimitiveCollectionsSupport { get; } } } diff --git a/src/EFCore.SingleStore/Infrastructure/Internal/SingleStoreOptionsExtension.cs b/src/EFCore.SingleStore/Infrastructure/Internal/SingleStoreOptionsExtension.cs index 8945706b5..90827543a 100644 --- a/src/EFCore.SingleStore/Infrastructure/Internal/SingleStoreOptionsExtension.cs +++ b/src/EFCore.SingleStore/Infrastructure/Internal/SingleStoreOptionsExtension.cs @@ -8,8 +8,11 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.DependencyInjection; using System.Collections.Generic; +using System.Data.Common; using System.Globalization; using Microsoft.EntityFrameworkCore; +using SingleStoreConnector; +using EntityFrameworkCore.SingleStore.Storage.Internal; namespace EntityFrameworkCore.SingleStore.Infrastructure.Internal { @@ -30,6 +33,7 @@ public SingleStoreOptionsExtension() public SingleStoreOptionsExtension([NotNull] SingleStoreOptionsExtension copyFrom) : base(copyFrom) { + DataSource = copyFrom.DataSource; ServerVersion = copyFrom.ServerVersion; NoBackslashEscapes = copyFrom.NoBackslashEscapes; UpdateSqlModeOnOpen = copyFrom.UpdateSqlModeOnOpen; @@ -54,6 +58,12 @@ public override DbContextOptionsExtensionInfo Info protected override RelationalOptionsExtension Clone() => new SingleStoreOptionsExtension(this); + /// + /// The , or if a connection string or was used + /// instead of a . + /// + public virtual DbDataSource DataSource { get; private set; } + /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. @@ -81,6 +91,36 @@ protected override RelationalOptionsExtension Clone() public virtual bool IndexOptimizedBooleanColumns { get; private set; } public virtual bool LimitKeyedOrIndexedStringColumnLength { get; private set; } public virtual bool StringComparisonTranslations { get; private set; } + public virtual bool PrimitiveCollectionsSupport { get; private set; } + + /// + /// Creates a new instance with all options the same as for this instance, but with the given option changed. + /// It is unusual to call this method directly. Instead use . + /// + /// The option to change. + /// A new instance with the option changed. + public virtual RelationalOptionsExtension WithDataSource(DbDataSource dataSource) + { + var clone = (SingleStoreOptionsExtension)Clone(); + clone.DataSource = dataSource; + return clone; + } + + /// + public override RelationalOptionsExtension WithConnectionString(string connectionString) + { + var clone = (SingleStoreOptionsExtension)base.WithConnectionString(connectionString); + clone.DataSource = null; + return clone; + } + + /// + public override RelationalOptionsExtension WithConnection(DbConnection connection) + { + var clone = (SingleStoreOptionsExtension)base.WithConnection(connection); + clone.DataSource = null; + return clone; + } /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used @@ -169,6 +209,13 @@ public virtual SingleStoreOptionsExtension WithStringComparisonTranslations(bool return clone; } + public virtual SingleStoreOptionsExtension WithPrimitiveCollectionsSupport(bool enable) + { + var clone = (SingleStoreOptionsExtension)Clone(); + clone.PrimitiveCollectionsSupport = enable; + return clone; + } + /// /// 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 @@ -205,7 +252,7 @@ public override string LogFragment if (Extension.ServerVersion != null) { - builder.Append("ServerVersion ") + builder.Append("ServerVersion=") .Append(Extension.ServerVersion) .Append(" "); } @@ -223,6 +270,7 @@ public override int GetServiceProviderHashCode() { var hashCode = new HashCode(); hashCode.Add(base.GetServiceProviderHashCode()); + hashCode.Add(Extension.DataSource?.ConnectionString); hashCode.Add(Extension.ServerVersion); hashCode.Add(Extension.NoBackslashEscapes); hashCode.Add(Extension.UpdateSqlModeOnOpen); @@ -233,6 +281,7 @@ public override int GetServiceProviderHashCode() hashCode.Add(Extension.IndexOptimizedBooleanColumns); hashCode.Add(Extension.LimitKeyedOrIndexedStringColumnLength); hashCode.Add(Extension.StringComparisonTranslations); + hashCode.Add(Extension.PrimitiveCollectionsSupport); _serviceProviderHash = hashCode.ToHashCode(); } @@ -243,6 +292,7 @@ public override int GetServiceProviderHashCode() public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) => other is ExtensionInfo otherInfo && base.ShouldUseSameServiceProvider(other) && + ReferenceEquals(Extension.DataSource, otherInfo.Extension.DataSource) && Equals(Extension.ServerVersion, otherInfo.Extension.ServerVersion) && Extension.NoBackslashEscapes == otherInfo.Extension.NoBackslashEscapes && Extension.UpdateSqlModeOnOpen == otherInfo.Extension.UpdateSqlModeOnOpen && @@ -252,7 +302,8 @@ public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo Extension.SchemaNameTranslator == otherInfo.Extension.SchemaNameTranslator && Extension.IndexOptimizedBooleanColumns == otherInfo.Extension.IndexOptimizedBooleanColumns && Extension.LimitKeyedOrIndexedStringColumnLength == otherInfo.Extension.LimitKeyedOrIndexedStringColumnLength && - Extension.StringComparisonTranslations == otherInfo.Extension.StringComparisonTranslations; + Extension.StringComparisonTranslations == otherInfo.Extension.StringComparisonTranslations && + Extension.PrimitiveCollectionsSupport == otherInfo.Extension.PrimitiveCollectionsSupport; public override void PopulateDebugInfo(IDictionary debugInfo) { @@ -266,6 +317,7 @@ public override void PopulateDebugInfo(IDictionary debugInfo) debugInfo["EntityFrameworkCore.SingleStore:" + nameof(SingleStoreDbContextOptionsBuilder.EnableIndexOptimizedBooleanColumns)] = HashCode.Combine(Extension.IndexOptimizedBooleanColumns).ToString(CultureInfo.InvariantCulture); debugInfo["EntityFrameworkCore.SingleStore:" + nameof(SingleStoreDbContextOptionsBuilder.LimitKeyedOrIndexedStringColumnLength)] = HashCode.Combine(Extension.LimitKeyedOrIndexedStringColumnLength).ToString(CultureInfo.InvariantCulture); debugInfo["EntityFrameworkCore.SingleStore:" + nameof(SingleStoreDbContextOptionsBuilder.EnableStringComparisonTranslations)] = HashCode.Combine(Extension.StringComparisonTranslations).ToString(CultureInfo.InvariantCulture); + debugInfo["EntityFrameworkCore.SingleStore:" + nameof(SingleStoreDbContextOptionsBuilder.EnablePrimitiveCollectionsSupport)] = HashCode.Combine(Extension.PrimitiveCollectionsSupport).ToString(CultureInfo.InvariantCulture); } } } diff --git a/src/EFCore.SingleStore/Infrastructure/ServerVersionSupport.cs b/src/EFCore.SingleStore/Infrastructure/ServerVersionSupport.cs index 1b84c19ee..080c2eee3 100644 --- a/src/EFCore.SingleStore/Infrastructure/ServerVersionSupport.cs +++ b/src/EFCore.SingleStore/Infrastructure/ServerVersionSupport.cs @@ -90,5 +90,17 @@ public virtual bool PropertyOrVersion(string propertyNameOrServerVersion) public virtual bool DescendingIndexes => false; public virtual bool Returning => false; public virtual bool CommonTableExpressions => false; + public virtual bool LimitWithinInAllAnySomeSubquery => false; + public virtual bool LimitWithNonConstantValue => false; + public virtual bool JsonTable => false; + public virtual bool JsonValue => false; + public virtual bool Values => false; + public virtual bool ValuesWithRows => false; + public virtual bool OffsetReferencesOuterQuery => false; + + public virtual bool JsonTableImplementationStable => JsonTable; + public virtual bool JsonTableImplementationWithoutMySqlBugs => JsonTable; + public virtual bool JsonTableImplementationUsingParameterAsSourceWithoutEngineCrash => JsonTable; + public virtual bool JsonTableImplementationWithAggregate => JsonTable; } } diff --git a/src/EFCore.SingleStore/Infrastructure/SingleStoreDbContextOptionsBuilder.cs b/src/EFCore.SingleStore/Infrastructure/SingleStoreDbContextOptionsBuilder.cs index 20f9083fb..f6d0caa57 100644 --- a/src/EFCore.SingleStore/Infrastructure/SingleStoreDbContextOptionsBuilder.cs +++ b/src/EFCore.SingleStore/Infrastructure/SingleStoreDbContextOptionsBuilder.cs @@ -123,5 +123,13 @@ public virtual SingleStoreDbContextOptionsBuilder LimitKeyedOrIndexedStringColum /// public virtual SingleStoreDbContextOptionsBuilder EnableStringComparisonTranslations(bool enable = true) => WithOption(e => e.WithStringComparisonTranslations(enable)); + + /// + /// Configures the context to translate using primitive collections. At the time of the Pomelo 8.0.0 release, MySQL Server can + /// crash when using primitive collections with JSON and MariaDB support is incomplete. Support and translations in regards to + /// this option can change at any time in the future. This optin is disabled by default. Enabled at your own risk. + /// + public virtual SingleStoreDbContextOptionsBuilder EnablePrimitiveCollectionsSupport(bool enable = true) + => WithOption(e => e.WithPrimitiveCollectionsSupport(enable)); } } diff --git a/src/EFCore.SingleStore/Infrastructure/SingleStoreServerVersion.cs b/src/EFCore.SingleStore/Infrastructure/SingleStoreServerVersion.cs index c390804f6..62d4fc01c 100644 --- a/src/EFCore.SingleStore/Infrastructure/SingleStoreServerVersion.cs +++ b/src/EFCore.SingleStore/Infrastructure/SingleStoreServerVersion.cs @@ -16,7 +16,7 @@ namespace Microsoft.EntityFrameworkCore public class SingleStoreServerVersion : ServerVersion { public static readonly string SingleStoreTypeIdentifier = nameof(ServerType.SingleStore).ToLowerInvariant(); - public static readonly ServerVersion LatestSupportedServerVersion = new SingleStoreServerVersion(new Version(8, 7, 0)); + public static readonly ServerVersion LatestSupportedServerVersion = new SingleStoreServerVersion(new Version(8, 9, 0)); public override ServerVersionSupport Supports { get; } @@ -90,6 +90,17 @@ internal SingleStoreServerVersionSupport([NotNull] ServerVersion serverVersion) public override bool ConnectionAttributes => ServerVersion.Version >= new Version(8, 1, 0); public override bool DescendingIndexes => false; public override bool CommonTableExpressions => false; + public override bool LimitWithinInAllAnySomeSubquery => false; + public override bool LimitWithNonConstantValue => false; + public override bool JsonTable => false; + public override bool JsonValue => false; + public override bool Values => false; + public override bool ValuesWithRows => false; + public override bool OffsetReferencesOuterQuery => false; + + public override bool JsonTableImplementationStable => false; + public override bool JsonTableImplementationWithoutMySqlBugs => false; // Other non-fatal bugs regarding JSON_TABLE. + public override bool JsonTableImplementationUsingParameterAsSourceWithoutEngineCrash => false; // MySQL non-deterministically crashes when using a parameter with JSON as the source of a JSON_TABLE call. } } } diff --git a/src/EFCore.SingleStore/Internal/SingleStoreLoggerExtensions.cs b/src/EFCore.SingleStore/Internal/SingleStoreLoggerExtensions.cs index 9b39a77a6..f7188b72c 100644 --- a/src/EFCore.SingleStore/Internal/SingleStoreLoggerExtensions.cs +++ b/src/EFCore.SingleStore/Internal/SingleStoreLoggerExtensions.cs @@ -24,7 +24,7 @@ private static string DecimalTypeKeyWarning(EventDefinitionBase definition, Even var p = (PropertyEventData)payload; return d.GenerateMessage( p.Property.Name, - p.Property.DeclaringEntityType.DisplayName()); + p.Property.DeclaringType.DisplayName()); } /// @@ -41,7 +41,7 @@ public static void DecimalTypeDefaultWarning( if (diagnostics.ShouldLog(definition)) { - definition.Log(diagnostics, property.Name, property.DeclaringEntityType.DisplayName()); + definition.Log(diagnostics, property.Name, property.DeclaringType.DisplayName()); } if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled)) @@ -61,7 +61,7 @@ private static string DecimalTypeDefaultWarning(EventDefinitionBase definition, var p = (PropertyEventData)payload; return d.GenerateMessage( p.Property.Name, - p.Property.DeclaringEntityType.DisplayName()); + p.Property.DeclaringType.DisplayName()); } /// @@ -78,7 +78,7 @@ public static void ByteIdentityColumnWarning( if (diagnostics.ShouldLog(definition)) { - definition.Log(diagnostics, property.Name, property.DeclaringEntityType.DisplayName()); + definition.Log(diagnostics, property.Name, property.DeclaringType.DisplayName()); } if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled)) @@ -98,7 +98,7 @@ private static string ByteIdentityColumnWarning(EventDefinitionBase definition, var p = (PropertyEventData)payload; return d.GenerateMessage( p.Property.Name, - p.Property.DeclaringEntityType.DisplayName()); + p.Property.DeclaringType.DisplayName()); } /// diff --git a/src/EFCore.SingleStore/Internal/SingleStoreModelValidator.cs b/src/EFCore.SingleStore/Internal/SingleStoreModelValidator.cs index 234dbf46b..cd5f8b75e 100644 --- a/src/EFCore.SingleStore/Internal/SingleStoreModelValidator.cs +++ b/src/EFCore.SingleStore/Internal/SingleStoreModelValidator.cs @@ -42,6 +42,20 @@ public SingleStoreModelValidator( { } + /// + protected override void ValidateJsonEntities( + IModel model, + IDiagnosticsLogger logger) + { + foreach (var entityType in model.GetEntityTypes()) + { + if (entityType.IsMappedToJson()) + { + throw new InvalidOperationException(SingleStoreStrings.Ef7CoreJsonMappingNotSupported); + } + } + } + /// protected override void ValidateStoredProcedures( IModel model, diff --git a/src/EFCore.SingleStore/Internal/SingleStoreOptions.cs b/src/EFCore.SingleStore/Internal/SingleStoreOptions.cs index a41240725..c80ba4019 100644 --- a/src/EFCore.SingleStore/Internal/SingleStoreOptions.cs +++ b/src/EFCore.SingleStore/Internal/SingleStoreOptions.cs @@ -3,6 +3,7 @@ // Licensed under the MIT. See LICENSE in the project root for license information. using System; +using System.Data.Common; using System.Linq; using EntityFrameworkCore.SingleStore.Infrastructure.Internal; using EntityFrameworkCore.SingleStore.Storage.Internal; @@ -10,6 +11,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using EntityFrameworkCore.SingleStore.Infrastructure; using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.Extensions.DependencyInjection; using SingleStoreConnector; namespace EntityFrameworkCore.SingleStore.Internal @@ -21,6 +23,7 @@ public class SingleStoreOptions : ISingleStoreOptions public SingleStoreOptions() { ConnectionSettings = new SingleStoreConnectionSettings(); + DataSource = null; ServerVersion = null; // We explicitly use `utf8` in all instances, where charset based calculations need to be done, but accessing annotations @@ -44,6 +47,7 @@ public SingleStoreOptions() LimitKeyedOrIndexedStringColumnLength = true; StringComparisonTranslations = false; + PrimitiveCollectionsSupport = false; } public virtual void Initialize(IDbContextOptions options) @@ -52,6 +56,7 @@ public virtual void Initialize(IDbContextOptions options) var mySqlJsonOptions = (SingleStoreJsonOptionsExtension)options.Extensions.LastOrDefault(e => e is SingleStoreJsonOptionsExtension); ConnectionSettings = GetConnectionSettings(mySqlOptions); + DataSource = mySqlOptions.DataSource; ServerVersion = mySqlOptions.ServerVersion ?? throw new InvalidOperationException($"The {nameof(ServerVersion)} has not been set."); NoBackslashEscapes = mySqlOptions.NoBackslashEscapes; ReplaceLineBreaksWithCharFunction = mySqlOptions.ReplaceLineBreaksWithCharFunction; @@ -63,6 +68,7 @@ public virtual void Initialize(IDbContextOptions options) JsonChangeTrackingOptions = mySqlJsonOptions?.JsonChangeTrackingOptions ?? default; LimitKeyedOrIndexedStringColumnLength = mySqlOptions.LimitKeyedOrIndexedStringColumnLength; StringComparisonTranslations = mySqlOptions.StringComparisonTranslations; + PrimitiveCollectionsSupport = mySqlOptions.PrimitiveCollectionsSupport; } public virtual void Validate(IDbContextOptions options) @@ -71,6 +77,24 @@ public virtual void Validate(IDbContextOptions options) var mySqlJsonOptions = (SingleStoreJsonOptionsExtension)options.Extensions.LastOrDefault(e => e is SingleStoreJsonOptionsExtension); var connectionSettings = GetConnectionSettings(mySqlOptions); + // + // CHECK: To we have to ensure that the ApplicationServiceProvider itself is not replaced, because we rely on it in our + // DbDataSource check, or is that not possible? + // + + // Even though we only save a DbDataSource that has been explicitly set using the SingleStoreOptionsExtensions here in SingleStoreOptions, + // we will later also fall back to a DbDataSource that has been added as a service to the ApplicationServiceProvider, if no + // DbDataSource has been explicitly set here. We call that DbDataSource the "effective" DbDataSource and handle it in the same + // way we would handle a singleton option. + var effectiveDataSource = mySqlOptions.DataSource ?? + options.FindExtension()?.ApplicationServiceProvider?.GetService(); + if (effectiveDataSource is not null && + !ReferenceEquals(DataSource, mySqlOptions.DataSource)) + { + throw new InvalidOperationException( + SingleStoreStrings.TwoDataSourcesInSameServiceProvider(nameof(DbContextOptionsBuilder.UseInternalServiceProvider))); + } + if (!Equals(ServerVersion, mySqlOptions.ServerVersion)) { throw new InvalidOperationException( @@ -162,6 +186,14 @@ public virtual void Validate(IDbContextOptions options) nameof(SingleStoreDbContextOptionsBuilder.EnableStringComparisonTranslations), nameof(DbContextOptionsBuilder.UseInternalServiceProvider))); } + + if (!Equals(PrimitiveCollectionsSupport, mySqlOptions.PrimitiveCollectionsSupport)) + { + throw new InvalidOperationException( + CoreStrings.SingletonOptionChanged( + nameof(SingleStoreDbContextOptionsBuilder.EnablePrimitiveCollectionsSupport), + nameof(DbContextOptionsBuilder.UseInternalServiceProvider))); + } } protected virtual SingleStoreDefaultDataTypeMappings ApplyDefaultDataTypeMappings(SingleStoreDefaultDataTypeMappings defaultDataTypeMappings, SingleStoreConnectionSettings connectionSettings) @@ -221,6 +253,7 @@ private static SingleStoreConnectionSettings GetConnectionSettings(SingleStoreOp protected virtual bool Equals(SingleStoreOptions other) { return Equals(ConnectionSettings, other.ConnectionSettings) && + ReferenceEquals(DataSource, other.DataSource) && Equals(ServerVersion, other.ServerVersion) && Equals(DefaultCharSet, other.DefaultCharSet) && Equals(NationalCharSet, other.NationalCharSet) && @@ -232,7 +265,8 @@ protected virtual bool Equals(SingleStoreOptions other) IndexOptimizedBooleanColumns == other.IndexOptimizedBooleanColumns && JsonChangeTrackingOptions == other.JsonChangeTrackingOptions && LimitKeyedOrIndexedStringColumnLength == other.LimitKeyedOrIndexedStringColumnLength && - StringComparisonTranslations == other.StringComparisonTranslations; + StringComparisonTranslations == other.StringComparisonTranslations && + PrimitiveCollectionsSupport == other.PrimitiveCollectionsSupport; } public override bool Equals(object obj) @@ -260,6 +294,7 @@ public override int GetHashCode() var hashCode = new HashCode(); hashCode.Add(ConnectionSettings); + hashCode.Add(DataSource?.ConnectionString); hashCode.Add(ServerVersion); hashCode.Add(DefaultCharSet); hashCode.Add(NationalCharSet); @@ -272,11 +307,18 @@ public override int GetHashCode() hashCode.Add(JsonChangeTrackingOptions); hashCode.Add(LimitKeyedOrIndexedStringColumnLength); hashCode.Add(StringComparisonTranslations); + hashCode.Add(PrimitiveCollectionsSupport); return hashCode.ToHashCode(); } public virtual SingleStoreConnectionSettings ConnectionSettings { get; private set; } + + /// + /// If null, there might still be a `DbDataSource` in the ApplicationServiceProvider. + /// + public virtual DbDataSource DataSource { get; private set; } + public virtual ServerVersion ServerVersion { get; private set; } public virtual CharSet DefaultCharSet { get; private set; } public virtual CharSet NationalCharSet { get; } @@ -289,5 +331,6 @@ public override int GetHashCode() public virtual SingleStoreJsonChangeTrackingOptions JsonChangeTrackingOptions { get; private set; } public virtual bool LimitKeyedOrIndexedStringColumnLength { get; private set; } public virtual bool StringComparisonTranslations { get; private set; } + public virtual bool PrimitiveCollectionsSupport { get; private set; } } } diff --git a/src/EFCore.SingleStore/Metadata/Builders/SingleStoreEntityTypeBuilder.cs b/src/EFCore.SingleStore/Metadata/Builders/SingleStoreEntityTypeBuilder.cs deleted file mode 100644 index 1088ad35f..000000000 --- a/src/EFCore.SingleStore/Metadata/Builders/SingleStoreEntityTypeBuilder.cs +++ /dev/null @@ -1,244 +0,0 @@ -// Copyright (c) Pomelo Foundation. All rights reserved. -// Copyright (c) SingleStore Inc. All rights reserved. -// Licensed under the MIT. See LICENSE in the project root for license information. - -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; -using System.Linq.Expressions; -using System.Reflection; -using Microsoft.EntityFrameworkCore.ChangeTracking; -using Microsoft.EntityFrameworkCore.Diagnostics; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Metadata.Internal; -using Microsoft.EntityFrameworkCore.Utilities; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace Pomelo.EntityFrameworkCore.SingleStore.Metadata.Builders -{ - public class SingleStoreEntityTypeBuilder : EntityTypeBuilder - { - public SingleStoreEntityTypeBuilder(IMutableEntityType entityType) : base(entityType) - { - - } - - private InternalEntityTypeBuilder Builder { [DebuggerStepThrough] get; } - - /// - /// - /// Configures a relationship where the target entity is owned by (or part of) this entity. - /// - /// - /// The target entity type for each ownership relationship is treated as a different entity type - /// even if the navigation is of the same type. Configuration of the target entity type - /// isn't applied to the target entity type of other ownership relationships. - /// - /// - /// Most operations on an owned entity require accessing it through the owner entity using the corresponding navigation. - /// - /// - /// After calling this method, you should chain a call to - /// to fully configure the relationship. - /// - /// - /// The name of the entity type that this relationship targets. - /// - /// The name of the reference navigation property on this entity type that represents the relationship. - /// - /// An object that can be used to configure the owned type and the relationship. - public override OwnedNavigationBuilder OwnsOne( - string ownedTypeName, - string navigationName) - => OwnsOneBuilder( - new TypeIdentity(Check.NotEmpty(ownedTypeName, nameof(ownedTypeName))), - Check.NotEmpty(navigationName, nameof(navigationName))); - - /// - /// - /// Configures a relationship where the target entity is owned by (or part of) this entity. - /// - /// - /// The target entity type for each ownership relationship is treated as a different entity type - /// even if the navigation is of the same type. Configuration of the target entity type - /// isn't applied to the target entity type of other ownership relationships. - /// - /// - /// Most operations on an owned entity require accessing it through the owner entity using the corresponding navigation. - /// - /// - /// After calling this method, you should chain a call to - /// to fully configure the relationship. - /// - /// - /// The name of the entity type that this relationship targets. - /// The CLR type of the entity type that this relationship targets. - /// - /// The name of the reference navigation property on this entity type that represents the relationship. - /// - /// An object that can be used to configure the owned type and the relationship. - public override OwnedNavigationBuilder OwnsOne( - string ownedTypeName, - Type ownedType, - string navigationName) - => OwnsOneBuilder( - new TypeIdentity(Check.NotEmpty(ownedTypeName, nameof(ownedTypeName)), ownedType), - Check.NotEmpty(navigationName, nameof(navigationName))); - - /// - /// - /// Configures a relationship where the target entity is owned by (or part of) this entity. - /// - /// - /// The target entity type for each ownership relationship is treated as a different entity type - /// even if the navigation is of the same type. Configuration of the target entity type - /// isn't applied to the target entity type of other ownership relationships. - /// - /// - /// Most operations on an owned entity require accessing it through the owner entity using the corresponding navigation. - /// - /// - /// After calling this method, you should chain a call to - /// to fully configure the relationship. - /// - /// - /// The entity type that this relationship targets. - /// - /// The name of the reference navigation property on this entity type that represents the relationship. - /// - /// An object that can be used to configure the owned type and the relationship. - public override OwnedNavigationBuilder OwnsOne( - Type ownedType, - string navigationName) - => OwnsOneBuilder( - new TypeIdentity(Check.NotNull(ownedType, nameof(ownedType)), (Model)Metadata.Model), - Check.NotEmpty(navigationName, nameof(navigationName))); - - /// - /// - /// Configures a relationship where the target entity is owned by (or part of) this entity. - /// - /// - /// The target entity type for each ownership relationship is treated as a different entity type - /// even if the navigation is of the same type. Configuration of the target entity type - /// isn't applied to the target entity type of other ownership relationships. - /// - /// - /// Most operations on an owned entity require accessing it through the owner entity using the corresponding navigation. - /// - /// - /// After calling this method, you should chain a call to - /// to fully configure the relationship. - /// - /// - /// The name of the entity type that this relationship targets. - /// - /// The name of the reference navigation property on this entity type that represents the relationship. - /// - /// An action that performs configuration of the owned type and the relationship. - /// An object that can be used to configure the entity type. - public override EntityTypeBuilder OwnsOne( - string ownedTypeName, - string navigationName, - Action buildAction) - { - Check.NotEmpty(ownedTypeName, nameof(ownedTypeName)); - Check.NotEmpty(navigationName, nameof(navigationName)); - Check.NotNull(buildAction, nameof(buildAction)); - - buildAction(OwnsOneBuilder(new TypeIdentity(ownedTypeName), navigationName)); - return this; - } - - /// - /// - /// Configures a relationship where the target entity is owned by (or part of) this entity. - /// - /// - /// The target entity type for each ownership relationship is treated as a different entity type - /// even if the navigation is of the same type. Configuration of the target entity type - /// isn't applied to the target entity type of other ownership relationships. - /// - /// - /// Most operations on an owned entity require accessing it through the owner entity using the corresponding navigation. - /// - /// - /// After calling this method, you should chain a call to - /// to fully configure the relationship. - /// - /// - /// The name of the entity type that this relationship targets. - /// The CLR type of the entity type that this relationship targets. - /// - /// The name of the reference navigation property on this entity type that represents the relationship. - /// - /// An action that performs configuration of the owned type and the relationship. - /// An object that can be used to configure the entity type. - public override EntityTypeBuilder OwnsOne( - string ownedTypeName, - Type ownedType, - string navigationName, - Action buildAction) - { - Check.NotEmpty(ownedTypeName, nameof(ownedTypeName)); - Check.NotNull(ownedType, nameof(ownedType)); - Check.NotEmpty(navigationName, nameof(navigationName)); - Check.NotNull(buildAction, nameof(buildAction)); - - buildAction(OwnsOneBuilder(new TypeIdentity(ownedTypeName, ownedType), navigationName)); - return this; - } - - /// - /// - /// Configures a relationship where the target entity is owned by (or part of) this entity. - /// - /// - /// The target entity type for each ownership relationship is treated as a different entity type - /// even if the navigation is of the same type. Configuration of the target entity type - /// isn't applied to the target entity type of other ownership relationships. - /// - /// - /// Most operations on an owned entity require accessing it through the owner entity using the corresponding navigation. - /// - /// - /// After calling this method, you should chain a call to - /// to fully configure the relationship. - /// - /// - /// The entity type that this relationship targets. - /// - /// The name of the reference navigation property on this entity type that represents the relationship. - /// - /// An action that performs configuration of the owned type and the relationship. - /// An object that can be used to configure the entity type. - public override EntityTypeBuilder OwnsOne( - Type ownedType, - string navigationName, - Action buildAction) - { - Check.NotNull(ownedType, nameof(ownedType)); - Check.NotEmpty(navigationName, nameof(navigationName)); - Check.NotNull(buildAction, nameof(buildAction)); - - buildAction(OwnsOneBuilder(new TypeIdentity(ownedType, (Model)Metadata.Model), navigationName)); - return this; - } - - private OwnedNavigationBuilder OwnsOneBuilder(in TypeIdentity ownedType, string navigationName) - { - IMutableForeignKey foreignKey; - using (var batch = Builder.Metadata.Model.DelayConventions()) - { - var navigationMember = new MemberIdentity(navigationName); - var relationship = Builder.HasOwnership(ownedType, navigationMember, ConfigurationSource.Explicit)!; - relationship.IsUnique(false, ConfigurationSource.Explicit); - foreignKey = (IMutableForeignKey)batch.Run(relationship.Metadata)!; - } - - return new OwnedNavigationBuilder(foreignKey); - } - } -} diff --git a/src/EFCore.SingleStore/Metadata/Conventions/TableCharSetAttributeConvention.cs b/src/EFCore.SingleStore/Metadata/Conventions/TableCharSetAttributeConvention.cs index 57108b418..75f94c7ba 100644 --- a/src/EFCore.SingleStore/Metadata/Conventions/TableCharSetAttributeConvention.cs +++ b/src/EFCore.SingleStore/Metadata/Conventions/TableCharSetAttributeConvention.cs @@ -12,7 +12,7 @@ namespace EntityFrameworkCore.SingleStore.Metadata.Conventions /// /// A convention that configures the character set for an entity based on the applied . /// - public class TableCharSetAttributeConvention : EntityTypeAttributeConventionBase + public class TableCharSetAttributeConvention : TypeAttributeConventionBase { /// /// Creates a new instance of . diff --git a/src/EFCore.SingleStore/Metadata/Conventions/TableCollationAttributeConvention.cs b/src/EFCore.SingleStore/Metadata/Conventions/TableCollationAttributeConvention.cs index 3e72eba54..3cc96efbd 100644 --- a/src/EFCore.SingleStore/Metadata/Conventions/TableCollationAttributeConvention.cs +++ b/src/EFCore.SingleStore/Metadata/Conventions/TableCollationAttributeConvention.cs @@ -12,7 +12,7 @@ namespace EntityFrameworkCore.SingleStore.Metadata.Conventions /// /// A convention that configures the collation for an entity based on the applied . /// - public class TableCollationAttributeConvention : EntityTypeAttributeConventionBase + public class TableCollationAttributeConvention : TypeAttributeConventionBase { /// /// Creates a new instance of . diff --git a/src/EFCore.SingleStore/Metadata/Internal/ObjectToEnumConverter.cs b/src/EFCore.SingleStore/Metadata/Internal/ObjectToEnumConverter.cs index b295cede3..fc126a3a3 100644 --- a/src/EFCore.SingleStore/Metadata/Internal/ObjectToEnumConverter.cs +++ b/src/EFCore.SingleStore/Metadata/Internal/ObjectToEnumConverter.cs @@ -13,7 +13,7 @@ public static class ObjectToEnumConverter /// be setup without provider specific dependencies. /// /// - /// See https://github.com/PomeloFoundation/EntityFrameworkCore.SingleStore/issues/1205 for further information. + /// See https://github.com/PomeloFoundation/EntityFrameworkCore.MySql/issues/1205 for further information. /// public static T? GetEnumValue(object value) where T : struct diff --git a/src/EFCore.SingleStore/Metadata/Internal/SingleStoreAnnotationProvider.cs b/src/EFCore.SingleStore/Metadata/Internal/SingleStoreAnnotationProvider.cs index 018d58e61..a61faea2d 100644 --- a/src/EFCore.SingleStore/Metadata/Internal/SingleStoreAnnotationProvider.cs +++ b/src/EFCore.SingleStore/Metadata/Internal/SingleStoreAnnotationProvider.cs @@ -61,7 +61,7 @@ public override IEnumerable For(ITable table, bool designTime) } // Model validation ensures that these facets are the same on all mapped entity types - var entityType = table.EntityTypeMappings.First().EntityType; + var entityType = (IEntityType)table.EntityTypeMappings.First().TypeBase; // Use an explicitly defined character set, if set. // Otherwise, explicitly use the model/database character set, if delegation is enabled. @@ -167,7 +167,7 @@ public override IEnumerable For(IColumn column, bool designTime) if (column.PropertyMappings.Where( m => (m.TableMapping.IsSharedTablePrincipal ?? true) && - m.TableMapping.EntityType == m.Property.DeclaringEntityType) + m.TableMapping.TypeBase == m.Property.DeclaringType) .Select(m => m.Property) .FirstOrDefault(p => p.GetValueGenerationStrategy(table) == SingleStoreValueGenerationStrategy.IdentityColumn) is IProperty identityProperty) { @@ -409,7 +409,8 @@ protected virtual string GetActualPropertyCharSet(IProperty[] properties, Delega properties.Select( p => p.FindTypeMapping() is SingleStoreStringTypeMapping {IsNationalChar: false} // An explicitly defined collation on the current property level takes precedence over an inherited charset. - ? GetActualEntityTypeCharSet(p.DeclaringEntityType, currentLevel) is string charSet && + ? p.DeclaringType is IEntityType entityType && + GetActualEntityTypeCharSet(entityType, currentLevel) is string charSet && (p.GetCollation() is not string collation || collation.StartsWith(charSet, StringComparison.OrdinalIgnoreCase)) ? charSet @@ -436,10 +437,11 @@ protected virtual string GetActualPropertyCollation(IProperty[] properties, Dele ? properties.Select(p => p.GetSingleStoreLegacyCollation()).FirstOrDefault(c => c is not null) ?? properties.Select( // An explicitly defined charset on the current property level takes precedence over an inherited collation. - p => (p.FindTypeMapping() is SingleStoreStringTypeMapping {IsNationalChar: false} - ? GetActualEntityTypeCollation(p.DeclaringEntityType, currentLevel) + p => (p.FindTypeMapping() is SingleStoreStringTypeMapping {IsNationalChar: false} && + p.DeclaringType is IEntityType entityType + ? GetActualEntityTypeCollation(entityType, currentLevel) : p.FindTypeMapping() is SingleStoreGuidTypeMapping {IsCharBasedStoreType: true} - ? p.DeclaringEntityType.Model.GetActualGuidCollation(_options.DefaultGuidCollation) + ? p.DeclaringType.Model.GetActualGuidCollation(_options.DefaultGuidCollation) : null) is string collation && (p.GetCharSet() is not string charSet || collation.StartsWith(charSet, StringComparison.OrdinalIgnoreCase)) diff --git a/src/EFCore.SingleStore/Migrations/SingleStoreMigrationsSqlGenerator.cs b/src/EFCore.SingleStore/Migrations/SingleStoreMigrationsSqlGenerator.cs index 03087f1b3..456c035f3 100644 --- a/src/EFCore.SingleStore/Migrations/SingleStoreMigrationsSqlGenerator.cs +++ b/src/EFCore.SingleStore/Migrations/SingleStoreMigrationsSqlGenerator.cs @@ -123,7 +123,7 @@ private void UpdateMigrationOperations(IReadOnlyList operati } catch (InvalidOperationException) { - throw new InvalidOperationException("Feature 'more than one FULLTEXT KEY' is not supported by SingleStore."); + throw new InvalidOperationException("Feature 'more than one FULLTEXT KEY' is not supported by SingleStore Distributed."); } } } @@ -434,16 +434,26 @@ protected override void Generate( { Check.NotNull(operation, nameof(operation)); Check.NotNull(builder, nameof(builder)); + if (!_options.ServerVersion.Supports.Sequences) { throw new InvalidOperationException( $"Cannot restart sequence '{operation.Name}' because sequences are not supported in server version {_options.ServerVersion}."); } + builder .Append("ALTER SEQUENCE ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name, operation.Schema)) - .Append(" RESTART WITH ") - .Append(IntegerConstant(operation.StartValue)) + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name, operation.Schema)); + + if (operation.StartValue.HasValue) + { + builder + .Append(" START WITH ") + .Append(IntegerConstant(operation.StartValue)); + } + + builder + .Append(" RESTART") .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); EndStatement(builder); @@ -1062,7 +1072,7 @@ protected override void DefaultValue( if (typeMapping is IDefaultValueCompatibilityAware defaultValueCompatibilityAware) { - typeMapping = defaultValueCompatibilityAware.Clone(true); + typeMapping = defaultValueCompatibilityAware.Clone(isDefaultValueCompatible: true); } var sqlLiteralDefaultValue = typeMapping.GenerateSqlLiteral(defaultValue); @@ -1184,7 +1194,7 @@ protected override void Generate( MigrationCommandListBuilder builder, bool terminate = true) { - // Feature 'FOREIGN KEY' is not supported by SingleStore. + // Feature 'FOREIGN KEY' is not supported by SingleStore Distributed. } protected override void ForeignKeyConstraint( @@ -1192,7 +1202,7 @@ protected override void ForeignKeyConstraint( IModel model, MigrationCommandListBuilder builder) { - // Feature 'FOREIGN KEY' is not supported by SingleStore. + // Feature 'FOREIGN KEY' is not supported by SingleStore Distributed. } protected override void PrimaryKeyConstraint( @@ -1395,7 +1405,7 @@ private string ColumnListWithIndexPrefixLengthAndSortOrder(MigrationOperation op protected virtual string ColumnList([NotNull] string[] columns, Func columnPostfix) => string.Join(", ", columns.Select((c, i) => Dependencies.SqlGenerationHelper.DelimitIdentifier(c) + columnPostfix?.Invoke(c, i))); - private string IntegerConstant(long value) + private string IntegerConstant(long? value) => string.Format(CultureInfo.InvariantCulture, "{0}", value); private static string Truncate(string source, int maxLength) diff --git a/src/EFCore.SingleStore/Properties/SingleStoreStrings.Designer.cs b/src/EFCore.SingleStore/Properties/SingleStoreStrings.Designer.cs index 25a08431b..bcd79474c 100644 --- a/src/EFCore.SingleStore/Properties/SingleStoreStrings.Designer.cs +++ b/src/EFCore.SingleStore/Properties/SingleStoreStrings.Designer.cs @@ -21,6 +21,14 @@ public static class SingleStoreStrings private static readonly ResourceManager _resourceManager = new ResourceManager("EntityFrameworkCore.SingleStore.Properties.SingleStoreStrings", typeof(SingleStoreStrings).GetTypeInfo().Assembly); + /// + /// Using two distinct data sources within a service provider is not supported, and Entity Framework is not building its own internal service provider. Either allow Entity Framework to build the service provider by removing the call to '{useInternalServiceProvider}', or ensure that the same data source is used for all uses of a given service provider passed to '{useInternalServiceProvider}'. + /// + public static string TwoDataSourcesInSameServiceProvider(object useInternalServiceProvider) + => string.Format( + GetString("TwoDataSourcesInSameServiceProvider", nameof(useInternalServiceProvider)), + useInternalServiceProvider); + /// /// Identity value generation cannot be used for the property '{property}' on entity type '{entityType}' because the property type is '{propertyType}'. Identity value generation can only be used with integer, DateTime, and DateTimeOffset properties. /// @@ -177,6 +185,12 @@ public static string StoredProcedureOutputParametersNotSupported(object entityTy GetString("StoredProcedureOutputParametersNotSupported", nameof(entityType), nameof(sproc)), entityType, sproc); + /// + /// The EF Core 7.0 JSON support isn't currently implemented. Instead, there is support for a more extensive implementation. See https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql for more information on how to map JSON. + /// + public static string Ef7CoreJsonMappingNotSupported + => GetString("Ef7CoreJsonMappingNotSupported"); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/EFCore.SingleStore/Properties/SingleStoreStrings.resx b/src/EFCore.SingleStore/Properties/SingleStoreStrings.resx index a5afc4c28..211dd0656 100644 --- a/src/EFCore.SingleStore/Properties/SingleStoreStrings.resx +++ b/src/EFCore.SingleStore/Properties/SingleStoreStrings.resx @@ -117,6 +117,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Using two distinct data sources within a service provider is not supported, and Entity Framework is not building its own internal service provider. Either allow Entity Framework to build the service provider by removing the call to '{useInternalServiceProvider}', or ensure that the same data source is used for all uses of a given service provider passed to '{useInternalServiceProvider}'. + Identity value generation cannot be used for the property '{property}' on entity type '{entityType}' because the property type is '{propertyType}'. Identity value generation can only be used with integer, DateTime, and DateTimeOffset properties. @@ -243,4 +246,7 @@ The entity type '{entityType}' is mapped to the stored procedure '{sproc}', which is configured with an output parameter. SingleStore stored procedures do not support output parameters; use result columns or return values instead. + + The EF Core 7.0 JSON support isn't currently implemented. Instead, there is support for a more extensive implementation. See https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql for more information on how to map JSON. + diff --git a/src/EFCore.SingleStore/Query/ExpressionTranslators/Internal/SingleStoreDbFunctionsExtensionsMethodTranslator.cs b/src/EFCore.SingleStore/Query/ExpressionTranslators/Internal/SingleStoreDbFunctionsExtensionsMethodTranslator.cs index ada6e0bbb..de4ea223c 100644 --- a/src/EFCore.SingleStore/Query/ExpressionTranslators/Internal/SingleStoreDbFunctionsExtensionsMethodTranslator.cs +++ b/src/EFCore.SingleStore/Query/ExpressionTranslators/Internal/SingleStoreDbFunctionsExtensionsMethodTranslator.cs @@ -72,6 +72,16 @@ private static readonly MethodInfo[] _likeMethodInfos && method.GetParameters().Length is >= 3 and <= 4) .SelectMany(method => _supportedLikeTypes.Select(type => method.MakeGenericMethod(type))).ToArray(); + private static readonly MethodInfo _isMatchMethodInfo + = typeof(SingleStoreDbFunctionsExtensions).GetRuntimeMethod( + nameof(SingleStoreDbFunctionsExtensions.IsMatch), + new[] {typeof(DbFunctions), typeof(string), typeof(string)}); + + private static readonly MethodInfo _isMatchWithMultiplePropertiesMethodInfo + = typeof(SingleStoreDbFunctionsExtensions).GetRuntimeMethod( + nameof(SingleStoreDbFunctionsExtensions.IsMatch), + new[] {typeof(DbFunctions), typeof(string[]), typeof(string)}); + private static readonly MethodInfo _matchMethodInfo = typeof(SingleStoreDbFunctionsExtensions).GetRuntimeMethod( nameof(SingleStoreDbFunctionsExtensions.Match), @@ -159,6 +169,14 @@ public virtual SqlExpression Translate( excapeChar); } + if (Equals(method, _isMatchMethodInfo) || + Equals(method, _isMatchWithMultiplePropertiesMethodInfo)) + { + return _sqlExpressionFactory.GreaterThan( + _sqlExpressionFactory.MakeMatch(arguments[1], arguments[2]), + _sqlExpressionFactory.Constant(0)); + } + if (Equals(method, _matchMethodInfo) || Equals(method, _matchWithMultiplePropertiesMethodInfo)) { diff --git a/src/EFCore.SingleStore/Query/ExpressionTranslators/Internal/SingleStoreJsonPocoTranslator.cs b/src/EFCore.SingleStore/Query/ExpressionTranslators/Internal/SingleStoreJsonPocoTranslator.cs index dc2415d39..c443642c0 100644 --- a/src/EFCore.SingleStore/Query/ExpressionTranslators/Internal/SingleStoreJsonPocoTranslator.cs +++ b/src/EFCore.SingleStore/Query/ExpressionTranslators/Internal/SingleStoreJsonPocoTranslator.cs @@ -28,7 +28,7 @@ public SingleStoreJsonPocoTranslator( { _typeMappingSource = typeMappingSource; _sqlExpressionFactory = sqlExpressionFactory; - _unquotedStringTypeMapping = ((SingleStoreStringTypeMapping)_typeMappingSource.FindMapping(typeof(string))).Clone(true); + _unquotedStringTypeMapping = ((SingleStoreStringTypeMapping)_typeMappingSource.FindMapping(typeof(string))).Clone(unquoted: true); _intTypeMapping = _typeMappingSource.FindMapping(typeof(int)); } diff --git a/src/EFCore.SingleStore/Query/ExpressionTranslators/Internal/SingleStoreJsonTableExpression.cs b/src/EFCore.SingleStore/Query/ExpressionTranslators/Internal/SingleStoreJsonTableExpression.cs new file mode 100644 index 000000000..2a143a00e --- /dev/null +++ b/src/EFCore.SingleStore/Query/ExpressionTranslators/Internal/SingleStoreJsonTableExpression.cs @@ -0,0 +1,269 @@ +// Copyright (c) Pomelo Foundation. All rights reserved. +// Copyright (c) SingleStore Inc. All rights reserved. +// Licensed under the MIT. See LICENSE in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; +using Microsoft.EntityFrameworkCore.Storage; + +namespace EntityFrameworkCore.SingleStore.Query.ExpressionTranslators.Internal; + +/// +/// An expression that represents a MySQL JSON_TABLE() function call in a SQL tree. +/// +public class SingleStoreJsonTableExpression : TableValuedFunctionExpression, IClonableTableExpressionBase +{ + /// + /// 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 virtual SqlExpression JsonExpression + => Arguments[0]; + + /// + /// 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 virtual IReadOnlyList Path { 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. + /// + public virtual IReadOnlyList ColumnInfos { 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. + /// + + public SingleStoreJsonTableExpression( + string alias, + SqlExpression jsonExpression, + IReadOnlyList path = null, + IReadOnlyList columnInfos = null) + : base(alias, "JSON_TABLE", schema: null, builtIn: true, new[] { jsonExpression }) + { + Path = path; + ColumnInfos = columnInfos; + } + + /// + /// 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. + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + { + var visitedJsonExpression = (SqlExpression)visitor.Visit(JsonExpression); + + PathSegment[] visitedPath = null; + + if (Path is not null) + { + for (var i = 0; i < Path.Count; i++) + { + var segment = Path[i]; + PathSegment newSegment; + + if (segment.PropertyName is not null) + { + // PropertyName segments are (currently) constants, nothing to visit. + newSegment = segment; + } + else + { + var newArrayIndex = (SqlExpression)visitor.Visit(segment.ArrayIndex)!; + if (newArrayIndex == segment.ArrayIndex) + { + newSegment = segment; + } + else + { + newSegment = new PathSegment(newArrayIndex); + + if (visitedPath is null) + { + visitedPath = new PathSegment[Path.Count]; + for (var j = 0; j < i; i++) + { + visitedPath[j] = Path[j]; + } + } + } + } + + if (visitedPath is not null) + { + visitedPath[i] = newSegment; + } + } + } + + return Update(visitedJsonExpression, visitedPath ?? Path, ColumnInfos); + } + + public override TableValuedFunctionExpression Update(IReadOnlyList arguments) + => Update(arguments[0], Path, ColumnInfos); + + /// + /// 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 virtual SingleStoreJsonTableExpression Update( + SqlExpression jsonExpression, + IReadOnlyList path, + IReadOnlyList columnInfos = null) + => Equals(jsonExpression, JsonExpression) + && (ReferenceEquals(path, Path) || path is not null && Path is not null && path.SequenceEqual(Path)) + && (ReferenceEquals(columnInfos, ColumnInfos) || columnInfos is not null && ColumnInfos is not null && columnInfos.SequenceEqual(ColumnInfos)) + ? this + : new SingleStoreJsonTableExpression(Alias, jsonExpression, path, columnInfos); + + + /// + /// 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. + /// + // TODO: Deep clone, see #30982 + public virtual TableExpressionBase Clone() + { + var clone = new SingleStoreJsonTableExpression(Alias, JsonExpression, Path, ColumnInfos); + + foreach (var annotation in GetAnnotations()) + { + clone.AddAnnotation(annotation.Name, annotation.Value); + } + + return clone; + } + + /// + /// 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. + /// + protected override void Print(ExpressionPrinter expressionPrinter) + { + expressionPrinter.Append(Name); + expressionPrinter.Append("("); + expressionPrinter.Visit(JsonExpression); + + var path = Path ?? Array.Empty(); + + expressionPrinter + .Append(", '$") + .Append(string.Join(".", path.Select(e => e.ToString()))) + .Append("'"); + + if (ColumnInfos is not null) + { + expressionPrinter.Append(" COLUMNS ("); + + for (var i = 0; i < ColumnInfos.Count; i++) + { + var columnInfo = ColumnInfos[i]; + + if (i > 0) + { + expressionPrinter.Append(", "); + } + + expressionPrinter + .Append(columnInfo.Name) + .Append(" ") + .Append(columnInfo.TypeMapping.StoreType); + + if (columnInfo.Path is not null) + { + expressionPrinter + .Append(" PATH '") + .Append(string.Join(".", columnInfo.Path.Select(e => e.ToString()))) + .Append("'"); + } + + if (columnInfo.AsJson) + { + expressionPrinter.Append(" AS JSON"); + } + } + + expressionPrinter.Append(")"); + } + + expressionPrinter.Append(")"); + + PrintAnnotations(expressionPrinter); + + expressionPrinter.Append(" AS "); + expressionPrinter.Append(Alias); + } + + /// + public override bool Equals(object obj) + => ReferenceEquals(this, obj) || (obj is SingleStoreJsonTableExpression jsonTableExpression && Equals(jsonTableExpression)); + + private bool Equals(SingleStoreJsonTableExpression other) + { + if (!base.Equals(other) || ColumnInfos?.Count != other.ColumnInfos?.Count) + { + return false; + } + + if (ReferenceEquals(ColumnInfos, other.ColumnInfos)) + { + return true; + } + + for (var i = 0; i < ColumnInfos!.Count; i++) + { + var (columnInfo, otherColumnInfo) = (ColumnInfos[i], other.ColumnInfos![i]); + + if (columnInfo.Name != otherColumnInfo.Name + || !columnInfo.TypeMapping.Equals(otherColumnInfo.TypeMapping) + || (columnInfo.Path is null != otherColumnInfo.Path is null + || (columnInfo.Path is not null + && otherColumnInfo.Path is not null + && columnInfo.Path.SequenceEqual(otherColumnInfo.Path)))) + { + return false; + } + } + + return true; + } + + /// + public override int GetHashCode() + => base.GetHashCode(); + + /// + /// 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 readonly record struct ColumnInfo( + string Name, + RelationalTypeMapping TypeMapping, + IReadOnlyList Path = null, + bool AsJson = false); +} diff --git a/src/EFCore.SingleStore/Query/ExpressionTranslators/Internal/SingleStoreStringComparisonMethodTranslator.cs b/src/EFCore.SingleStore/Query/ExpressionTranslators/Internal/SingleStoreStringComparisonMethodTranslator.cs index f11f8ab72..ebf5c15ef 100644 --- a/src/EFCore.SingleStore/Query/ExpressionTranslators/Internal/SingleStoreStringComparisonMethodTranslator.cs +++ b/src/EFCore.SingleStore/Query/ExpressionTranslators/Internal/SingleStoreStringComparisonMethodTranslator.cs @@ -4,20 +4,22 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Linq.Expressions; using System.Reflection; using System.Text; using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; +using Microsoft.EntityFrameworkCore.Storage; using EntityFrameworkCore.SingleStore.Infrastructure.Internal; using EntityFrameworkCore.SingleStore.Internal; using EntityFrameworkCore.SingleStore.Query.Internal; namespace EntityFrameworkCore.SingleStore.Query.ExpressionTranslators.Internal { - public class SingleStoreStringComparisonMethodTranslator : IMethodCallTranslator + public class SingleStoreStringComparisonMethodTranslator : SingleStoreQueryCompilationContextMethodTranslator { private static readonly MethodInfo _equalsMethodInfo = typeof(string).GetRuntimeMethod(nameof(string.Equals), new[] {typeof(string), typeof(StringComparison)}); @@ -57,24 +59,35 @@ private static readonly MethodInfo _indexOfMethodInfoWithStartIndexArg _staticEqualsMethodInfo, }; - private readonly SqlExpression _caseSensitiveComparisons; + private readonly IReadOnlyList _caseSensitiveComparisons; private readonly SingleStoreSqlExpressionFactory _sqlExpressionFactory; private readonly ISingleStoreOptions _options; - public SingleStoreStringComparisonMethodTranslator(ISqlExpressionFactory sqlExpressionFactory, ISingleStoreOptions options) + private static readonly MethodInfo _escapeLikePatternParameterMethod = + typeof(SingleStoreStringComparisonMethodTranslator).GetTypeInfo().GetDeclaredMethod(nameof(ConstructLikePatternParameter))!; + + public SingleStoreStringComparisonMethodTranslator( + ISqlExpressionFactory sqlExpressionFactory, + Func queryCompilationContextResolver, + ISingleStoreOptions options) + : base(queryCompilationContextResolver) { _sqlExpressionFactory = (SingleStoreSqlExpressionFactory)sqlExpressionFactory; _options = options; - _caseSensitiveComparisons = _sqlExpressionFactory.Constant( - new[] {StringComparison.Ordinal, StringComparison.CurrentCulture, StringComparison.InvariantCulture}); + _caseSensitiveComparisons = new[] + { + _sqlExpressionFactory.Constant(StringComparison.Ordinal), + _sqlExpressionFactory.Constant(StringComparison.CurrentCulture), + _sqlExpressionFactory.Constant(StringComparison.InvariantCulture) + }.ToList().AsReadOnly(); } - public virtual SqlExpression Translate( + public override SqlExpression Translate( SqlExpression instance, MethodInfo method, IReadOnlyList arguments, - IDiagnosticsLogger logger) + QueryCompilationContext queryCompilationContext) { if(_options.StringComparisonTranslations) { @@ -91,29 +104,17 @@ public virtual SqlExpression Translate( if (Equals(method, _startsWithMethodInfo)) { - return MakeStartsWithExpression( - instance, - arguments[0], - arguments[1] - ); + return MakeStartsWithExpression(queryCompilationContext, instance, arguments[0], arguments[1]); } if (Equals(method, _endsWithMethodInfo)) { - return MakeEndsWithExpression( - instance, - arguments[0], - arguments[1] - ); + return MakeEndsWithExpression(queryCompilationContext, instance, arguments[0], arguments[1]); } if (Equals(method, _containsMethodInfo)) { - return MakeContainsExpression( - instance, - arguments[0], - arguments[1] - ); + return MakeContainsExpression(queryCompilationContext, instance, arguments[0], arguments[1]); } if (Equals(method, _indexOfMethodInfo)) @@ -182,7 +183,7 @@ public virtual SqlExpression MakeStringEqualsExpression( new[] { new CaseWhenClause( - _sqlExpressionFactory.In(stringComparison, _caseSensitiveComparisons, false), + _sqlExpressionFactory.In(stringComparison, _caseSensitiveComparisons), // Case sensitive, accent sensitive _sqlExpressionFactory.Equal( leftValue, @@ -198,6 +199,7 @@ public virtual SqlExpression MakeStringEqualsExpression( } public virtual SqlExpression MakeStartsWithExpression( + QueryCompilationContext queryCompilationContext, [NotNull] SqlExpression target, [NotNull] SqlExpression prefix, [CanBeNull] SqlExpression stringComparison = null) @@ -205,6 +207,7 @@ public virtual SqlExpression MakeStartsWithExpression( if (stringComparison == null) { return MakeStartsWithEndsWithExpressionImpl( + queryCompilationContext, target, e => e, prefix, @@ -217,12 +220,14 @@ public virtual SqlExpression MakeStartsWithExpression( return CreateExpressionForCaseSensitivity( cmp, () => MakeStartsWithEndsWithExpressionImpl( + queryCompilationContext, target, e => e, prefix, e => Utf8Bin(e), true), () => MakeStartsWithEndsWithExpressionImpl( + queryCompilationContext, LCase(target), e => LCase(e), prefix, @@ -236,10 +241,10 @@ public virtual SqlExpression MakeStartsWithExpression( new CaseWhenClause( _sqlExpressionFactory.In( stringComparison, - _caseSensitiveComparisons, - false), + _caseSensitiveComparisons), // Case sensitive, accent sensitive MakeStartsWithEndsWithExpressionImpl( + queryCompilationContext, target, e => e, prefix, @@ -248,6 +253,7 @@ public virtual SqlExpression MakeStartsWithExpression( }, // Case insensitive, accent sensitive MakeStartsWithEndsWithExpressionImpl( + queryCompilationContext, target, e => LCase(e), prefix, @@ -256,6 +262,7 @@ public virtual SqlExpression MakeStartsWithExpression( } public virtual SqlExpression MakeEndsWithExpression( + QueryCompilationContext queryCompilationContext, [NotNull] SqlExpression target, [NotNull] SqlExpression suffix, [CanBeNull] SqlExpression stringComparison = null) @@ -263,6 +270,7 @@ public virtual SqlExpression MakeEndsWithExpression( if (stringComparison == null) { return MakeStartsWithEndsWithExpressionImpl( + queryCompilationContext, target, e => e, suffix, @@ -275,12 +283,14 @@ public virtual SqlExpression MakeEndsWithExpression( return CreateExpressionForCaseSensitivity( cmp, () => MakeStartsWithEndsWithExpressionImpl( + queryCompilationContext, target, e => e, suffix, e => Utf8Bin(e), false), () => MakeStartsWithEndsWithExpressionImpl( + queryCompilationContext, target, e => LCase(e), suffix, @@ -294,10 +304,10 @@ public virtual SqlExpression MakeEndsWithExpression( new CaseWhenClause( _sqlExpressionFactory.In( stringComparison, - _caseSensitiveComparisons, - false), + _caseSensitiveComparisons), // Case sensitive, accent sensitive MakeStartsWithEndsWithExpressionImpl( + queryCompilationContext, target, e => e, suffix, @@ -306,6 +316,7 @@ public virtual SqlExpression MakeEndsWithExpression( }, // Case insensitive, accent sensitive MakeStartsWithEndsWithExpressionImpl( + queryCompilationContext, target, e => LCase(e), suffix, @@ -314,6 +325,7 @@ public virtual SqlExpression MakeEndsWithExpression( } public virtual SqlExpression MakeContainsExpression( + QueryCompilationContext queryCompilationContext, [NotNull] SqlExpression target, [NotNull] SqlExpression search, [CanBeNull] SqlExpression stringComparison = null) @@ -323,6 +335,7 @@ public virtual SqlExpression MakeContainsExpression( if (stringComparison == null) { return MakeContainsExpressionImpl( + queryCompilationContext, target, e => e, search, @@ -335,6 +348,7 @@ public virtual SqlExpression MakeContainsExpression( cmp, () => MakeContainsExpressionImpl( + queryCompilationContext, target, e => e, search, @@ -342,6 +356,7 @@ public virtual SqlExpression MakeContainsExpression( ), () => MakeContainsExpressionImpl( + queryCompilationContext, target, e => LCase(e), search, @@ -354,9 +369,10 @@ public virtual SqlExpression MakeContainsExpression( new[] { new CaseWhenClause( - _sqlExpressionFactory.In(stringComparison, _caseSensitiveComparisons, false), + _sqlExpressionFactory.In(stringComparison, _caseSensitiveComparisons), // Case sensitive, accent sensitive MakeContainsExpressionImpl( + queryCompilationContext, target, e => e, search, @@ -366,6 +382,7 @@ public virtual SqlExpression MakeContainsExpression( }, // Case insensitive, accent sensitive MakeContainsExpressionImpl( + queryCompilationContext, target, e => LCase(e), search, @@ -375,6 +392,7 @@ public virtual SqlExpression MakeContainsExpression( } private SqlExpression MakeStartsWithEndsWithExpressionImpl( + QueryCompilationContext queryCompilationContext, SqlExpression target, [NotNull] Func targetTransform, SqlExpression prefixSuffix, @@ -389,57 +407,54 @@ private SqlExpression MakeStartsWithEndsWithExpressionImpl( { // The prefix is constant. Aside from null or empty, we escape all special characters (%, _, \) // in C# and send a simple LIKE. - if (constantPrefixSuffixExpression.Value is string constantPrefixSuffixString) + return constantPrefixSuffixExpression.Value switch { - // TRUE (pattern == "") - // something LIKE 'foo%' (pattern != "", StartsWith()) - // something LIKE '%foo' (pattern != "", EndsWith()) - return constantPrefixSuffixString == string.Empty - ? (SqlExpression)_sqlExpressionFactory.Constant(true) - : _sqlExpressionFactory.Like( - targetTransform(target), - prefixSuffixTransform( - _sqlExpressionFactory.Constant( - (startsWith - ? string.Empty - : "%") + - EscapeLikePattern(constantPrefixSuffixString) + - (startsWith - ? "%" - : string.Empty)))); - } + null => _sqlExpressionFactory.Like(targetTransform(target), _sqlExpressionFactory.Constant(null, stringTypeMapping)), + "" => _sqlExpressionFactory.Like(targetTransform(target), _sqlExpressionFactory.Constant("%")), + string s => _sqlExpressionFactory.Like( + targetTransform(target), + prefixSuffixTransform( + _sqlExpressionFactory.Constant( + $"{(startsWith ? string.Empty : "%")}{(s.Any(IsLikeWildChar) ? EscapeLikePattern(s) : s)}{(startsWith ? "%" : string.Empty)}"))), + _ => throw new UnreachableException(), + }; + } - // TODO: EF Core 5 - // https://github.com/PomeloFoundation/EntityFrameworkCore.SingleStore/issues/996#issuecomment-607876040 - // Can return NULL in .NET 5 after https://github.com/dotnet/efcore/issues/20498 has been fixed. - // `something LIKE NULL` always returns `NULL`. We will return `false`, to indicate, that no match - // could be found, because returning a constant of `NULL` will throw later in EF Core when used as - // a predicate. - // return _sqlExpressionFactory.Constant(null, RelationalTypeMapping.NullMapping); - // This results in NULL anyway, but works around EF Core's inability to handle predicates that are - // constant null values. - return _sqlExpressionFactory.Like(target, _sqlExpressionFactory.Constant(null, stringTypeMapping)); + if (GetLikeExpressionUsingParameter( + queryCompilationContext, + target, + targetTransform, + prefixSuffix, + stringTypeMapping, + startsWith + ? StartsEndsWithContains.StartsWith + : StartsEndsWithContains.EndsWith) is { } likeExpressionUsingParameter) + { + return likeExpressionUsingParameter; } // TODO: Generally, LEFT & compare is faster than escaping potential pattern characters with REPLACE(). // However, this might not be the case, if the pattern is constant after all (e.g. `LCASE('fo%o')`), in // which case, `something LIKE CONCAT(REPLACE(REPLACE(LCASE('fo%o'), '%', '\\%'), '_', '\\_'), '%')` should // be faster than `LEFT(something, CHAR_LENGTH('fo%o')) = LCASE('fo%o')`. - // See https://github.com/PomeloFoundation/EntityFrameworkCore.SingleStore/issues/996#issuecomment-607733553 - - // The prefix is non-constant, we use LEFT to extract the substring and compare. - return _sqlExpressionFactory.Equal( - _sqlExpressionFactory.NullableFunction( - startsWith - ? "LEFT" - : "RIGHT", - new[] {targetTransform(target), CharLength(prefixSuffix)}, - typeof(string), - stringTypeMapping), - prefixSuffixTransform(prefixSuffix)); + // See https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql/issues/996#issuecomment-607733553 + + // The prefix is non-constant, we use LEFT/RIGHT to extract the substring and compare. + return _sqlExpressionFactory.AndAlso( + _sqlExpressionFactory.IsNotNull(targetTransform(target)), + _sqlExpressionFactory.AndAlso( + _sqlExpressionFactory.IsNotNull(prefixSuffix), + _sqlExpressionFactory.Equal( + _sqlExpressionFactory.NullableFunction( + startsWith ? "LEFT" : "RIGHT", + new[] { targetTransform(target), CharLength(prefixSuffix), }, + typeof(string), + stringTypeMapping), + prefixSuffixTransform(prefixSuffix)))); } private SqlExpression MakeContainsExpressionImpl( + QueryCompilationContext queryCompilationContext, SqlExpression target, [NotNull] Func targetTransform, SqlExpression pattern, @@ -453,41 +468,86 @@ private SqlExpression MakeContainsExpressionImpl( { // The prefix is constant. Aside from null or empty, we escape all special characters (%, _, \) // in C# and send a simple LIKE. - if (constantPatternExpression.Value is string constantPatternString) + // The prefix is constant. Aside from null or empty, we escape all special characters (%, _, \) + // in C# and send a simple LIKE. + return constantPatternExpression.Value switch { - // TRUE (pattern == "") - // something LIKE '%foo%' (pattern != "") - return constantPatternString == string.Empty - ? (SqlExpression)_sqlExpressionFactory.Constant(true) - : _sqlExpressionFactory.Like( - targetTransform(target), - patternTransform(_sqlExpressionFactory.Constant('%' + EscapeLikePattern(constantPatternString) + '%'))); - } + null => _sqlExpressionFactory.Like(targetTransform(target), _sqlExpressionFactory.Constant(null, stringTypeMapping)), + "" => _sqlExpressionFactory.Like(targetTransform(target), _sqlExpressionFactory.Constant("%")), + string s => _sqlExpressionFactory.Like( + targetTransform(target), + patternTransform(_sqlExpressionFactory.Constant($"%{(s.Any(IsLikeWildChar) ? EscapeLikePattern(s) : s)}%"))), + _ => throw new UnreachableException(), + }; + } - // TODO: EF Core 5 - // https://github.com/PomeloFoundation/EntityFrameworkCore.SingleStore/issues/996#issuecomment-607876040 - // Can return NULL in .NET 5 after https://github.com/dotnet/efcore/issues/20498 has been fixed. - // `something LIKE NULL` always returns `NULL`. We will return `false`, to indicate, that no match - // could be found, because returning a constant of `NULL` will throw later in EF Core when used as - // a predicate. - // return _sqlExpressionFactory.Constant(null, RelationalTypeMapping.NullMapping); - // This results in NULL anyway, but works around EF Core's inability to handle predicates that are - // constant null values. - return _sqlExpressionFactory.Like(target, _sqlExpressionFactory.Constant(null, stringTypeMapping)); + if (GetLikeExpressionUsingParameter( + queryCompilationContext, + target, + targetTransform, + pattern, + stringTypeMapping, + StartsEndsWithContains.Contains) is { } likeExpressionUsingParameter) + { + return likeExpressionUsingParameter; } // 'foo' LIKE '' OR LOCATE('foo', 'barfoobar') > 0 // This cannot be "' ' = '' OR ..", because ' ' would be trimmed to '' when using equals, but not when using LIKE. // Using an empty pattern `LOCATE('', 'barfoobar')` returns 1. - return _sqlExpressionFactory.OrElse( - _sqlExpressionFactory.Like( - pattern, - _sqlExpressionFactory.Constant(string.Empty, stringTypeMapping)), - _sqlExpressionFactory.GreaterThan( - Locate(patternTransform(pattern), targetTransform(target)), - _sqlExpressionFactory.Constant(0))); + // return _sqlExpressionFactory.OrElse( + // _sqlExpressionFactory.Like( + // pattern, + // _sqlExpressionFactory.Constant(string.Empty, stringTypeMapping)), + // _sqlExpressionFactory.GreaterThan( + // Locate(patternTransform(pattern), targetTransform(target)), + // _sqlExpressionFactory.Constant(0))); + + // For Contains, just use CHARINDEX and check if the result is greater than 0. + // Add a check to return null when the pattern is an empty string (and the string isn't null) + return _sqlExpressionFactory.AndAlso( + _sqlExpressionFactory.IsNotNull(target), + _sqlExpressionFactory.AndAlso( + _sqlExpressionFactory.IsNotNull(pattern), + _sqlExpressionFactory.OrElse( + _sqlExpressionFactory.GreaterThan( + Locate(patternTransform(pattern), targetTransform(target)), + _sqlExpressionFactory.Constant(0)), + _sqlExpressionFactory.Like( + pattern, + _sqlExpressionFactory.Constant(string.Empty, stringTypeMapping))))); } + protected virtual SqlExpression GetLikeExpressionUsingParameter(QueryCompilationContext queryCompilationContext, + SqlExpression target, + Func targetTransform, + SqlExpression pattern, + RelationalTypeMapping stringTypeMapping, + StartsEndsWithContains methodType) + { + if (pattern is SqlParameterExpression patternParameter && + patternParameter.Name.StartsWith(QueryCompilationContext.QueryParameterPrefix, StringComparison.Ordinal)) + { + // The pattern is a parameter, register a runtime parameter that will contain the rewritten LIKE pattern, where + // all special characters have been escaped. + var lambda = Expression.Lambda( + Expression.Call( + _escapeLikePatternParameterMethod, + QueryCompilationContext.QueryContextParameter, + Expression.Constant(patternParameter.Name), + Expression.Constant(methodType)), + QueryCompilationContext.QueryContextParameter); + + var escapedPatternParameter = + queryCompilationContext.RegisterRuntimeParameter(patternParameter.Name + "_rewritten", lambda); + + return _sqlExpressionFactory.Like( + targetTransform(target), + new SqlParameterExpression(escapedPatternParameter.Name!, escapedPatternParameter.Type, stringTypeMapping)); + } + + return null; + } public virtual SqlExpression MakeIndexOfExpression( [NotNull] SqlExpression target, [NotNull] SqlExpression search, @@ -534,8 +594,7 @@ public virtual SqlExpression MakeIndexOfExpression( new CaseWhenClause( _sqlExpressionFactory.In( stringComparison, - _caseSensitiveComparisons, - false), + _caseSensitiveComparisons), // Case sensitive, accent sensitive MakeIndexOfExpressionImpl( target, @@ -652,5 +711,46 @@ private static string EscapeLikePattern(string pattern) return builder.ToString(); } + + private static string ConstructLikePatternParameter( + QueryContext queryContext, + string baseParameterName, + StartsEndsWithContains methodType) + => queryContext.ParameterValues[baseParameterName] switch + { + null => null, + + // In .NET, all strings start/end with the empty string, but SQL LIKE return false for empty patterns. + // Return % which always matches instead. + "" => "%", + + string s => methodType switch + { + StartsEndsWithContains.StartsWith => EscapeLikePattern(s) + '%', + StartsEndsWithContains.EndsWith => '%' + EscapeLikePattern(s), + StartsEndsWithContains.Contains => $"%{EscapeLikePattern(s)}%", + _ => throw new ArgumentOutOfRangeException(nameof(methodType), methodType, null) + }, + + _ => throw new UnreachableException() + }; + + protected enum StartsEndsWithContains + { + /// + /// StartsWith => LIKE 'foo%' + /// + StartsWith, + + /// + /// EndsWith => LIKE '%foo' + /// + EndsWith, + + /// + /// Contains => LIKE '%foo%' + /// + Contains + } } } diff --git a/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreBoolOptimizingExpressionVisitor.cs b/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreBoolOptimizingExpressionVisitor.cs index 49ffc4d49..6dd7ad7bb 100644 --- a/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreBoolOptimizingExpressionVisitor.cs +++ b/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreBoolOptimizingExpressionVisitor.cs @@ -14,7 +14,7 @@ namespace EntityFrameworkCore.SingleStore.Query.ExpressionVisitors.Internal { /// /// "WHERE `boolColumn`" doesn't use available indices, while "WHERE `boolColumn` = TRUE" does. - /// See https://github.com/PomeloFoundation/EntityFrameworkCore.SingleStore/issues/1104 + /// See https://github.com/PomeloFoundation/EntityFrameworkCore.MySql/issues/1104 /// public class SingleStoreBoolOptimizingExpressionVisitor : SqlExpressionVisitor { @@ -134,17 +134,41 @@ protected override Expression VisitFromSql(FromSqlExpression fromSqlExpression) protected override Expression VisitIn(InExpression inExpression) { - Check.NotNull(inExpression, nameof(inExpression)); - var parentOptimize = _optimize; _optimize = false; var item = (SqlExpression)Visit(inExpression.Item); var subquery = (SelectExpression)Visit(inExpression.Subquery); - var values = (SqlExpression)Visit(inExpression.Values); + + var values = inExpression.Values; + SqlExpression[] newValues = null; + if (values is not null) + { + for (var i = 0; i < values.Count; i++) + { + var value = values[i]; + var newValue = (SqlExpression)Visit(value); + + if (newValue != value && newValues is null) + { + newValues = new SqlExpression[values.Count]; + for (var j = 0; j < i; j++) + { + newValues[j] = values[j]; + } + } + + if (newValues is not null) + { + newValues[i] = newValue; + } + } + } + + var valuesParameter = (SqlParameterExpression)Visit(inExpression.ValuesParameter); _optimize = parentOptimize; - return ApplyConversion(inExpression.Update(item, values, subquery), condition: true); + return ApplyConversion(inExpression.Update(item, subquery, newValues ?? values, valuesParameter), condition: true); } protected override Expression VisitLike(LikeExpression likeExpression) @@ -517,6 +541,21 @@ protected override Expression VisitLeftJoin(LeftJoinExpression leftJoinExpressio return leftJoinExpression.Update(table, joinPredicate); } + protected override Expression VisitRowValue(RowValueExpression rowValueExpression) + { + var parentOptimize = _optimize; + _optimize = false; + + var values = new SqlExpression[rowValueExpression.Values.Count]; + for (var i = 0; i < values.Length; i++) + { + values[i] = (SqlExpression)Visit(rowValueExpression.Values[i]); + } + + _optimize = parentOptimize; + return rowValueExpression.Update(values); + } + protected override Expression VisitScalarSubquery(ScalarSubqueryExpression scalarSubqueryExpression) { Check.NotNull(scalarSubqueryExpression, nameof(scalarSubqueryExpression)); @@ -626,6 +665,21 @@ protected override Expression VisitUpdate(UpdateExpression updateExpression) } protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExpression) - => jsonScalarExpression; + => ApplyConversion(jsonScalarExpression, condition: false); + + protected override Expression VisitValues(ValuesExpression valuesExpression) + { + var parentOptimize = _optimize; + _optimize = false; + + var rowValues = new RowValueExpression[valuesExpression.RowValues.Count]; + for (var i = 0; i < rowValues.Length; i++) + { + rowValues[i] = (RowValueExpression)Visit(valuesExpression.RowValues[i]); + } + + _optimize = parentOptimize; + return valuesExpression.Update(rowValues); + } } } diff --git a/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreBug96947WorkaroundExpressionVisitor.cs b/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreBug96947WorkaroundExpressionVisitor.cs index bb08574e7..9e77b0469 100644 --- a/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreBug96947WorkaroundExpressionVisitor.cs +++ b/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreBug96947WorkaroundExpressionVisitor.cs @@ -16,7 +16,7 @@ namespace EntityFrameworkCore.SingleStore.Query.ExpressionVisitors.Internal /// /// See https://bugs.mysql.com/bug.php?id=96947 /// https://github.com/OData/WebApi/issues/2124 - /// https://github.com/PomeloFoundation/EntityFrameworkCore.SingleStore/issues/1293 + /// https://github.com/PomeloFoundation/EntityFrameworkCore.MySql/issues/1293 /// public class SingleStoreBug96947WorkaroundExpressionVisitor : ExpressionVisitor { diff --git a/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreCompatibilityExpressionVisitor.cs b/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreCompatibilityExpressionVisitor.cs index a45907aff..663f3711d 100644 --- a/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreCompatibilityExpressionVisitor.cs +++ b/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreCompatibilityExpressionVisitor.cs @@ -3,12 +3,15 @@ // Licensed under the MIT. See LICENSE in the project root for license information. using System; +using System.Collections.Generic; +using System.Linq; using System.Linq.Expressions; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using EntityFrameworkCore.SingleStore.Infrastructure.Internal; +using EntityFrameworkCore.SingleStore.Query.ExpressionTranslators.Internal; namespace EntityFrameworkCore.SingleStore.Query.ExpressionVisitors.Internal { @@ -16,6 +19,11 @@ public class SingleStoreCompatibilityExpressionVisitor : ExpressionVisitor { private readonly ISingleStoreOptions _options; + private SelectExpression _currentSelectExpression; + private SelectExpression _parentSelectExpression; + + private readonly SingleStoreContainsAggregateFunctionExpressionVisitor _mySqlContainsAggregateFunctionExpressionVisitor = new SingleStoreContainsAggregateFunctionExpressionVisitor(); + public SingleStoreCompatibilityExpressionVisitor(ISingleStoreOptions options) { _options = options; @@ -28,7 +36,12 @@ protected override Expression VisitExtension(Expression extensionExpression) CrossApplyExpression crossApplyExpression => VisitCrossApply(crossApplyExpression), OuterApplyExpression outerApplyExpression => VisitOuterApply(outerApplyExpression), ExceptExpression exceptExpression => VisitExcept(exceptExpression), - IntersectExpression intersectExpression => VisitIntercept(intersectExpression), + IntersectExpression intersectExpression => VisitIntersect(intersectExpression), + JsonScalarExpression jsonScalarExpression => VisitJsonScalar(jsonScalarExpression), + SingleStoreJsonTableExpression jsonTableExpression => VisitJsonTable(jsonTableExpression), + + SelectExpression selectExpression => VisitSelect(selectExpression), + ShapedQueryExpression shapedQueryExpression => shapedQueryExpression.Update(Visit(shapedQueryExpression.QueryExpression), Visit(shapedQueryExpression.ShaperExpression)), _ => base.VisitExtension(extensionExpression) }; @@ -45,9 +58,75 @@ protected virtual Expression VisitOuterApply(OuterApplyExpression outerApplyExpr protected virtual Expression VisitExcept(ExceptExpression exceptExpression) => CheckSupport(exceptExpression, _options.ServerVersion.Supports.ExceptIntercept); - protected virtual Expression VisitIntercept(IntersectExpression intersectExpression) + protected virtual Expression VisitIntersect(IntersectExpression intersectExpression) => CheckSupport(intersectExpression, _options.ServerVersion.Supports.ExceptIntercept); + protected virtual Expression VisitJsonScalar(JsonScalarExpression jsonScalarExpression) + => CheckSupport(jsonScalarExpression, _options.ServerVersion.Supports.Json); + + protected virtual Expression VisitJsonTable(SingleStoreJsonTableExpression jsonTableExpression) + { + if (!_options.ServerVersion.Supports.JsonTable) + { + return CheckSupport(jsonTableExpression, false); + } + + if (!_options.ServerVersion.Supports.JsonTableImplementationWithAggregate && + _mySqlContainsAggregateFunctionExpressionVisitor.ProcessSelect(_currentSelectExpression)) + { + throw new InvalidOperationException($"JSON_TABLE() does not support aggregates on {_options.ServerVersion} and would return unexpected results if used."); + } + + if (!_options.ServerVersion.Supports.OuterApply && + jsonTableExpression.JsonExpression is ColumnExpression columnExpression && + _parentSelectExpression is not null && + _parentSelectExpression.Tables.All(t => t.Alias != columnExpression.TableAlias)) + { + throw new InvalidOperationException($"JSON_TABLE() does not support references to an outer query that is not the immediate parent on {_options.ServerVersion}."); + } + + return jsonTableExpression; + } + + protected virtual Expression VisitSelect(SelectExpression selectExpression) + { + var grandParentSelectExpression = _parentSelectExpression; + _parentSelectExpression = _currentSelectExpression; + _currentSelectExpression = selectExpression; + + foreach (var item in selectExpression.Projection) + { + Visit(item); + } + + foreach (var table in selectExpression.Tables) + { + Visit(table); + } + + Visit(selectExpression.Predicate); + + foreach (var groupingKey in selectExpression.GroupBy) + { + Visit(groupingKey); + } + + Visit(selectExpression.Having); + + foreach (var ordering in selectExpression.Orderings) + { + Visit(ordering.Expression); + } + + Visit(selectExpression.Offset); + Visit(selectExpression.Limit); + + _currentSelectExpression = _parentSelectExpression; + _parentSelectExpression = grandParentSelectExpression; + + return selectExpression; + } + protected virtual Expression CheckSupport(Expression expression, bool isSupported) => CheckTranslated( isSupported diff --git a/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreContainsAggregateFunctionExpressionVisitor.cs b/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreContainsAggregateFunctionExpressionVisitor.cs new file mode 100644 index 000000000..8c83680a8 --- /dev/null +++ b/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreContainsAggregateFunctionExpressionVisitor.cs @@ -0,0 +1,114 @@ +// Copyright (c) Pomelo Foundation. All rights reserved. +// Copyright (c) SingleStore Inc. All rights reserved. +// Licensed under the MIT. See LICENSE in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; + +namespace EntityFrameworkCore.SingleStore.Query.ExpressionVisitors.Internal; + +/// +/// Looks for aggregate functions (like SUM(), AVG() etc.) in an expression tree, but not in subqueries. +/// +public sealed class SingleStoreContainsAggregateFunctionExpressionVisitor : ExpressionVisitor +{ + // See https://dev.mysql.com/doc/refman/8.0/en/aggregate-functions.html + // See https://docs.singlestore.com/cloud/reference/sql-reference/aggregate-functions/ + private static readonly SortedSet _aggregateFunctions = new SortedSet(StringComparer.OrdinalIgnoreCase) + { + "ANY_VALUE", + "APPROX_PERCENTILE", + "AVG", + "BIT_AND", + "BIT_OR", + "BIT_XOR", + "COUNT", + "GROUP_CONCAT", + "MAX", + "MEDIAN", + "MIN", + "MOD", + "STD", + "STDDEV", + "STDDEV_POP", + "STDDEV_SAMP", + "SUM", + "VAR_SAMP", + "VARIANCE", + }; + + public bool AggregateFunctionFound { get; private set; } + + public bool ProcessUntilSelect(Expression node) + { + // Can be reused within the same thread. + AggregateFunctionFound = false; + + Visit(node); + + return AggregateFunctionFound; + } + + public bool ProcessSelect(SelectExpression selectExpression) + { + // Can be reused within the same thread. + AggregateFunctionFound = false; + + foreach (var item in selectExpression.Projection) + { + Visit(item); + } + + foreach (var table in selectExpression.Tables) + { + Visit(table); + } + + Visit(selectExpression.Predicate); + + foreach (var groupingKey in selectExpression.GroupBy) + { + Visit(groupingKey); + } + + Visit(selectExpression.Having); + + foreach (var ordering in selectExpression.Orderings) + { + Visit(ordering.Expression); + } + + Visit(selectExpression.Offset); + Visit(selectExpression.Limit); + + return AggregateFunctionFound; + } + + public override Expression Visit(Expression node) + => AggregateFunctionFound + ? node + : base.Visit(node); + + protected override Expression VisitExtension(Expression extensionExpression) + => extensionExpression switch + { + SqlFunctionExpression sqlFunctionExpression => VisitSqlFunction(sqlFunctionExpression), + SelectExpression selectExpression => selectExpression, + ShapedQueryExpression shapedQueryExpression => shapedQueryExpression, + _ => base.VisitExtension(extensionExpression) + }; + + private Expression VisitSqlFunction(SqlFunctionExpression sqlFunctionExpression) + { + if (_aggregateFunctions.Contains(sqlFunctionExpression.Name)) + { + AggregateFunctionFound = true; + return sqlFunctionExpression; + } + + return base.VisitExtension(sqlFunctionExpression); + } +} diff --git a/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreHavingExpressionVisitor.cs b/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreHavingExpressionVisitor.cs index 0d77fbc2c..df237865d 100644 --- a/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreHavingExpressionVisitor.cs +++ b/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreHavingExpressionVisitor.cs @@ -2,8 +2,6 @@ // Copyright (c) SingleStore Inc. All rights reserved. // Licensed under the MIT. See LICENSE in the project root for license information. -using System; -using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using Microsoft.EntityFrameworkCore.Query; @@ -44,7 +42,7 @@ havingExpression is not SqlConstantExpression && havingExpression is not SingleStoreColumnAliasReferenceExpression) { _containsAggregateFunctionExpressionVisitor ??= new SingleStoreContainsAggregateFunctionExpressionVisitor(); - if (!_containsAggregateFunctionExpressionVisitor.Process(havingExpression)) + if (!_containsAggregateFunctionExpressionVisitor.ProcessUntilSelect(havingExpression)) { selectExpression.PushdownIntoSubquery(); var subQuery = (SelectExpression) selectExpression.Tables.Single(); @@ -87,71 +85,5 @@ havingExpression is not SqlConstantExpression && return base.VisitExtension(selectExpression); } - - /// - /// Looks for aggregate functions (like SUM(), AVG() etc.) in an expression tree, but not in subqueries. - /// - private sealed class SingleStoreContainsAggregateFunctionExpressionVisitor : ExpressionVisitor - { - // See https://dev.mysql.com/doc/refman/8.0/en/aggregate-functions.html - private static readonly SortedSet _aggregateFunctions = new SortedSet(StringComparer.OrdinalIgnoreCase) - { - "AVG", - "BIT_AND", - "BIT_OR", - "BIT_XOR", - "COUNT", - "GROUP_CONCAT", - "JSON_ARRAYAGG", - "JSON_OBJECTAGG", - "MAX", - "MIN", - "STD", - "STDDEV", - "STDDEV_POP", - "STDDEV_SAMP", - "SUM", - "VAR_POP", - "VAR_SAMP", - "VARIANCE", - }; - - public bool AggregateFunctionFound { get; private set; } - - public bool Process(Expression node) - { - // Can be reused within the same thread. - AggregateFunctionFound = false; - - Visit(node); - - return AggregateFunctionFound; - } - - public override Expression Visit(Expression node) - => AggregateFunctionFound - ? node - : base.Visit(node); - - protected override Expression VisitExtension(Expression extensionExpression) - => extensionExpression switch - { - SqlFunctionExpression sqlFunctionExpression => VisitSqlFunction(sqlFunctionExpression), - SelectExpression selectExpression => selectExpression, - ShapedQueryExpression shapedQueryExpression => shapedQueryExpression, - _ => base.VisitExtension(extensionExpression) - }; - - private Expression VisitSqlFunction(SqlFunctionExpression sqlFunctionExpression) - { - if (_aggregateFunctions.Contains(sqlFunctionExpression.Name)) - { - AggregateFunctionFound = true; - return sqlFunctionExpression; - } - - return base.VisitExtension(sqlFunctionExpression); - } - } } } diff --git a/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreParameterInliningExpressionVisitor.cs b/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreParameterInliningExpressionVisitor.cs new file mode 100644 index 000000000..df5af5dcd --- /dev/null +++ b/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreParameterInliningExpressionVisitor.cs @@ -0,0 +1,98 @@ +// Copyright (c) Pomelo Foundation. All rights reserved. +// Copyright (c) SingleStore Inc. All rights reserved. +// Licensed under the MIT. See LICENSE in the project root for license information. + +using System.Collections.Generic; +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Utilities; +using EntityFrameworkCore.SingleStore.Infrastructure.Internal; +using EntityFrameworkCore.SingleStore.Query.Expressions.Internal; +using EntityFrameworkCore.SingleStore.Query.ExpressionTranslators.Internal; + +namespace EntityFrameworkCore.SingleStore.Query.ExpressionVisitors.Internal; + +/// +/// Inject parameter inlining expressions where parameters are not supported for some reason. +/// +public class SingleStoreParameterInliningExpressionVisitor : ExpressionVisitor +{ + private readonly IRelationalTypeMappingSource _typeMappingSource; + private readonly ISqlExpressionFactory _sqlExpressionFactory; + private readonly ISingleStoreOptions _options; + + private IReadOnlyDictionary _parametersValues; + private bool _canCache; + + private bool _inJsonTableSourceParameterCall; + + public SingleStoreParameterInliningExpressionVisitor( + IRelationalTypeMappingSource typeMappingSource, + ISqlExpressionFactory sqlExpressionFactory, + ISingleStoreOptions options) + { + _typeMappingSource = typeMappingSource; + _sqlExpressionFactory = sqlExpressionFactory; + _options = options; + } + + public virtual Expression Process(Expression expression, IReadOnlyDictionary parametersValues, out bool canCache) + { + Check.NotNull(expression, nameof(expression)); + + _parametersValues = parametersValues; + _canCache = true; + + var result = Visit(expression); + + canCache = _canCache; + + return result; + } + + protected override Expression VisitExtension(Expression extensionExpression) + => extensionExpression switch + { + SingleStoreJsonTableExpression jsonTableExpression => VisitJsonTable(jsonTableExpression), + SqlParameterExpression sqlParameterExpression => VisitSqlParameter(sqlParameterExpression), + ShapedQueryExpression shapedQueryExpression => shapedQueryExpression.Update( + Visit(shapedQueryExpression.QueryExpression), + Visit(shapedQueryExpression.ShaperExpression)), + _ => base.VisitExtension(extensionExpression) + }; + + protected virtual Expression VisitJsonTable(SingleStoreJsonTableExpression jsonTableExpression) + { + var parentInJsonTableSourceParameterCall = _inJsonTableSourceParameterCall; + _inJsonTableSourceParameterCall = true; + var jsonExpression = (SqlExpression)Visit(jsonTableExpression.JsonExpression); + _inJsonTableSourceParameterCall = parentInJsonTableSourceParameterCall; + + return jsonTableExpression.Update( + jsonExpression, + jsonTableExpression.Path, + jsonTableExpression.ColumnInfos); + } + + protected virtual Expression VisitSqlParameter(SqlParameterExpression sqlParameterExpression) + { + // For test simplicity, we currently inline parameters even for non MySQL database engines (even though it should not be necessary + // for e.g. MariaDB). + // TODO: Use inlined parameters only if JsonTableImplementationUsingParameterAsSourceWithoutEngineCrash is true. + if (_inJsonTableSourceParameterCall /*&& + !_options.ServerVersion.Supports.JsonTableImplementationUsingParameterAsSourceWithoutEngineCrash*/) + { + _canCache = false; + + return new SingleStoreInlinedParameterExpression( + sqlParameterExpression, + _sqlExpressionFactory.Constant( + _parametersValues[sqlParameterExpression.Name], + sqlParameterExpression.TypeMapping)); + } + + return sqlParameterExpression; + } +} diff --git a/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreQuerySqlGenerator.cs b/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreQuerySqlGenerator.cs index e0b2de808..481beb65a 100644 --- a/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreQuerySqlGenerator.cs +++ b/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreQuerySqlGenerator.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Linq.Expressions; using JetBrains.Annotations; @@ -15,6 +16,7 @@ using Microsoft.EntityFrameworkCore.Utilities; using EntityFrameworkCore.SingleStore.Infrastructure.Internal; using EntityFrameworkCore.SingleStore.Query.Expressions.Internal; +using EntityFrameworkCore.SingleStore.Query.ExpressionTranslators.Internal; using EntityFrameworkCore.SingleStore.Query.Internal; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; @@ -36,11 +38,14 @@ public class SingleStoreQuerySqlGenerator : QuerySqlGenerator { "float", new []{ "float" } }, { "binary", new []{ "binary", "varbinary", "tinyblob", "blob", "mediumblob", "longblob" } }, { "datetime(6)", new []{ "datetime(6)" } }, + { "datetime(3)", new []{ "datetime(3)" } }, { "datetime", new []{ "datetime" } }, { "date", new []{ "date" } }, { "timestamp(6)", new []{ "timestamp(6)" } }, + { "timestamp(3)", new []{ "timestamp(3)" } }, { "timestamp", new []{ "timestamp" } }, { "time(6)", new []{ "time(6)" } }, + { "time(3)", new []{ "time(3)" } }, { "time", new []{ "time" } }, { "json", new []{ "json" } }, { "char", new []{ "char", "varchar", "text", "tinytext", "mediumtext", "longtext" } }, @@ -49,6 +54,7 @@ public class SingleStoreQuerySqlGenerator : QuerySqlGenerator private const ulong LimitUpperBound = 18446744073709551610; + private readonly IRelationalTypeMappingSource _typeMappingSource; private readonly ISingleStoreOptions _options; private string _removeTableAliasOld; private string _removeTableAliasNew; @@ -59,9 +65,11 @@ public class SingleStoreQuerySqlGenerator : QuerySqlGenerator /// public SingleStoreQuerySqlGenerator( [NotNull] QuerySqlGeneratorDependencies dependencies, + IRelationalTypeMappingSource typeMappingSource, [CanBeNull] ISingleStoreOptions options) : base(dependencies) { + _typeMappingSource = typeMappingSource; _options = options; } @@ -70,6 +78,8 @@ protected override Expression VisitExtension(Expression extensionExpression) { SingleStoreJsonTraversalExpression jsonTraversalExpression => VisitJsonPathTraversal(jsonTraversalExpression), SingleStoreColumnAliasReferenceExpression columnAliasReferenceExpression => VisitColumnAliasReference(columnAliasReferenceExpression), + SingleStoreJsonTableExpression jsonTableExpression => VisitJsonTableExpression(jsonTableExpression), + SingleStoreInlinedParameterExpression inlinedParameterExpression => VisitInlinedParameterExpression(inlinedParameterExpression), _ => base.VisitExtension(extensionExpression) }; @@ -80,6 +90,13 @@ private Expression VisitColumnAliasReference(SingleStoreColumnAliasReferenceExpr return columnAliasReferenceExpression; } + private Expression VisitInlinedParameterExpression(SingleStoreInlinedParameterExpression inlinedParameterExpression) + { + Visit(inlinedParameterExpression.ValueExpression); + + return inlinedParameterExpression; + } + protected virtual Expression VisitJsonPathTraversal(SingleStoreJsonTraversalExpression expression) { // If the path contains parameters, then the -> and ->> aliases are not supported by MySQL, because @@ -257,7 +274,8 @@ protected override Expression VisitOuterApply(OuterApplyExpression outerApplyExp { Sql.Append("LEFT JOIN "); - if (outerApplyExpression.Table is not TableExpression) + if (outerApplyExpression.Table is not TableExpression && + outerApplyExpression.Table is not SingleStoreJsonTableExpression) { Sql.Append("LATERAL "); } @@ -319,7 +337,7 @@ protected override Expression VisitSqlBinary(SqlBinaryExpression sqlBinaryExpres // EF uses unary Equal and NotEqual to represent is-null checking. // These need to be surrounded with parenthesis in various cases (e.g. where TRUE = x IS NOT NULL). - // See https://github.com/PomeloFoundation/EntityFrameworkCore.SingleStore/issues/1309 + // See https://github.com/PomeloFoundation/EntityFrameworkCore.MySql/issues/1309 requiresBrackets = RequiresBrackets(sqlBinaryExpression.Right) || !requiresBrackets && sqlBinaryExpression.Right is SqlUnaryExpression sqlUnaryExpression && @@ -408,7 +426,31 @@ protected override Expression VisitUpdate(UpdateExpression updateExpression) && selectExpression.Projection.Count == 0) { Sql.Append("UPDATE "); - GenerateList(selectExpression.Tables, e => Visit(e), sql => sql.AppendLine()); + + if (selectExpression.Tables.Count > 1) + { + var tables = selectExpression.Tables; + + if (selectExpression.Tables.All(t => !updateExpression.Table.Equals(t is JoinExpressionBase join ? join.Table : t))) + { + Visit(updateExpression.Table); + Sql.AppendLine(","); + + if (tables[0] is not JoinExpressionBase) + { + tables = tables + .Skip(1) + .Prepend(new CrossJoinExpression(tables[0])) + .ToArray(); + } + } + + GenerateList(tables, e => Visit(e), sql => sql.AppendLine()); + } + else + { + Visit(updateExpression.Table); + } Sql.AppendLine().Append("SET "); Visit(updateExpression.ColumnValueSetters[0].Column); @@ -441,6 +483,144 @@ protected override Expression VisitUpdate(UpdateExpression updateExpression) RelationalStrings.ExecuteOperationWithUnsupportedOperatorInSqlGeneration(nameof(RelationalQueryableExtensions.ExecuteUpdate))); } + protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExpression) + { + // TODO: Stop producing empty JsonScalarExpressions, #30768 + var path = jsonScalarExpression.Path; + if (path.Count == 0) + { + return jsonScalarExpression; + } + + var jsonPathNeedsConcat = JsonPathNeedsConcat(jsonScalarExpression.Path); + var useJsonValue = !jsonPathNeedsConcat && _options.ServerVersion.Supports.JsonValue; + var jsonFunctionName = useJsonValue ? "JSON_VALUE" : "JSON_UNQUOTE(JSON_EXTRACT"; + string castStoreType = null; + + // if (/*jsonScalarExpression.TypeMapping is SqlServerJsonTypeMapping + // ||*/ jsonScalarExpression.TypeMapping?.ElementTypeMapping is not null) + // { + // jsonFunctionName = "JSON_UNQUOTE(JSON_EXTRACT"; + // } + // else + // { + // // JSON_VALUE returns varchar(512) by default (https://dev.mysql.com/doc/refman/8.0/en/json-search-functions.html#function_json-value), + // // so we let it cast the result to the expected type using the RETURNING clause. + // // CHECK: - except if it's a string (since the cast interferes with indexes over the JSON property). + // // if (jsonScalarExpression.TypeMapping is not StringTypeMapping) + // // { + // castStoreType = GetCastStoreType(jsonScalarExpression.TypeMapping); + // // } + // + // jsonFunctionName = "JSON_VALUE"; + // } + + // if (jsonScalarExpression.TypeMapping?.ElementTypeMapping is null && + // jsonScalarExpression.TypeMapping is not StringTypeMapping && + // jsonPathNeedsConcat) + // { + castStoreType = GetCastStoreType(jsonScalarExpression.TypeMapping); + // } + + if (castStoreType is not null) + { + Sql.Append("CAST("); + } + + Sql.Append(jsonFunctionName); + Sql.Append("("); + + Visit(jsonScalarExpression.Json); + + Sql.Append(", "); + GenerateJsonPath(jsonScalarExpression.Path); + Sql.Append(useJsonValue ? ")" : "))"); + + if (castStoreType is not null) + { + Sql.Append(" AS "); + Sql.Append(castStoreType); + Sql.Append(")"); + } + + return jsonScalarExpression; + } + + protected override void GenerateValues(ValuesExpression valuesExpression) + { + if (_options.ServerVersion.Supports.Values || + _options.ServerVersion.Supports.ValuesWithRows) + { + base.GenerateValues(valuesExpression); + return; + } + + var rowValues = valuesExpression.RowValues; + + // + // Use backwards compatible SELECT statements: + // + + Sql.Append("SELECT "); + + Check.DebugAssert(rowValues.Count > 0, "rowValues.Count > 0"); + var firstRowValues = rowValues[0].Values; + for (var i = 0; i < firstRowValues.Count; i++) + { + if (i > 0) + { + Sql.Append(", "); + } + + Visit(firstRowValues[i]); + + Sql + .Append(AliasSeparator) + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(valuesExpression.ColumnNames[i])); + } + + if (rowValues.Count > 1) + { + for (var r = 1; r < rowValues.Count; r++) + { + Sql.Append(" UNION ALL SELECT "); + Visit(rowValues[r]); + } + } + } + + protected override Expression VisitRowValue(RowValueExpression rowValueExpression) + { + if (_options.ServerVersion.Supports.Values) + { + return base.VisitRowValue(rowValueExpression); + } + + if (_options.ServerVersion.Supports.ValuesWithRows) + { + Sql.Append("ROW"); + return base.VisitRowValue(rowValueExpression); + } + + // + // Columns for backwards compatible SELECT statement: + // + + var values = rowValueExpression.Values; + var count = values.Count; + for (var i = 0; i < count; i++) + { + if (i > 0) + { + Sql.Append(", "); + } + + Visit(values[i]); + } + + return rowValueExpression; + } + protected virtual void GenerateList( IReadOnlyList items, Action generationAction, @@ -685,6 +865,152 @@ public virtual Expression VisitSingleStoreBinaryExpression(SingleStoreBinaryExpr return mySqlBinaryExpression; } + protected virtual Expression VisitJsonTableExpression(SingleStoreJsonTableExpression jsonTableExpression) + { + // if (jsonTableExpression.ColumnInfos is not { Count: > 0 }) + // { + // var hasStringElement = jsonTableExpression.JsonExpression.TypeMapping?.ElementTypeMapping?.ClrType == typeof(string); + // + // if (hasStringElement) + // { + // Sql.Append("JSON_UNQUOTE("); + // } + // + // Sql.Append("JSON_EXTRACT("); + // Visit(jsonTableExpression.JsonExpression); + // Sql.Append(", "); + // GenerateJsonPath(jsonTableExpression.Path); + // Sql.Append(")"); + // + // if (hasStringElement) + // { + // Sql.Append(")"); + // } + // + // return jsonTableExpression; + // } + + Sql.Append("JSON_TABLE("); + + Visit(jsonTableExpression.JsonExpression); + + Sql.Append(", "); + GenerateJsonPath(jsonTableExpression.Path); + + if (jsonTableExpression.ColumnInfos is not { Count: > 0 }) + { + throw new InvalidOperationException("JSON_TABLE expression does not contain any columns."); + } + + Sql.AppendLine(" COLUMNS ("); + + using (var _ = Sql.Indent()) + { + Sql.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier("key")); + Sql.AppendLine(" FOR ORDINALITY,"); + + for (var i = 0; i < jsonTableExpression.ColumnInfos.Count; i++) + { + var columnInfo = jsonTableExpression.ColumnInfos[i]; + + if (i > 0) + { + Sql.AppendLine(","); + } + + GenerateColumnInfo(columnInfo); + } + + Sql.AppendLine(); + } + + Sql.Append(")"); + + void GenerateColumnInfo(SingleStoreJsonTableExpression.ColumnInfo columnInfo) + { + Sql + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(columnInfo.Name)) + .Append(" ") + .Append(columnInfo.TypeMapping.StoreType); + + if (columnInfo.Path is not null) + { + Sql.Append(" PATH "); + GenerateJsonPath(columnInfo.Path); + } + + // if (columnInfo.AsJson) + // { + // Sql.Append(" AS ").Append("JSON"); + // } + } + + Sql.Append(")"); + Sql.Append(AliasSeparator).Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(jsonTableExpression.Alias)); + + return jsonTableExpression; + } + + protected virtual bool JsonPathNeedsConcat(IReadOnlyList path) + => path.Any(s => s.ArrayIndex is not null && s.ArrayIndex is not SqlConstantExpression); + + protected virtual void GenerateJsonPath(IReadOnlyList path, bool? needsConcat = null) + { + path ??= Array.Empty(); + needsConcat ??= JsonPathNeedsConcat(path); + + if (needsConcat.Value) + { + Sql.Append("CONCAT("); + } + + Sql.Append("'$"); + + foreach (var pathSegment in path) + { + switch (pathSegment) + { + case { PropertyName: string propertyName }: + Sql.Append(".").Append(propertyName); + break; + + case { ArrayIndex: SqlExpression arrayIndex }: + Sql.Append("["); + + if (arrayIndex is SqlConstantExpression) + { + Visit(arrayIndex); + } + else + { + Sql.Append("', "); + + Visit( + new SqlUnaryExpression( + ExpressionType.Convert, + arrayIndex, + typeof(string), + _typeMappingSource.GetMapping(typeof(string)))); + + Sql.Append(", '"); + } + + Sql.Append("]"); + break; + + default: + throw new ArgumentOutOfRangeException(); + } + } + + Sql.Append("'"); + + if (needsConcat.Value) + { + Sql.Append(")"); + } + } + /// protected override void CheckComposableSql(string sql) { diff --git a/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreQuerySqlGeneratorFactory.cs b/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreQuerySqlGeneratorFactory.cs index 8299f407b..cd1caa3bc 100644 --- a/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreQuerySqlGeneratorFactory.cs +++ b/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreQuerySqlGeneratorFactory.cs @@ -4,6 +4,7 @@ using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Storage; using EntityFrameworkCore.SingleStore.Infrastructure.Internal; namespace EntityFrameworkCore.SingleStore.Query.ExpressionVisitors.Internal @@ -11,17 +12,20 @@ namespace EntityFrameworkCore.SingleStore.Query.ExpressionVisitors.Internal public class SingleStoreQuerySqlGeneratorFactory : IQuerySqlGeneratorFactory { private readonly QuerySqlGeneratorDependencies _dependencies; + private readonly IRelationalTypeMappingSource _typeMappingSource; private readonly ISingleStoreOptions _options; public SingleStoreQuerySqlGeneratorFactory( [NotNull] QuerySqlGeneratorDependencies dependencies, + IRelationalTypeMappingSource typeMappingSource, ISingleStoreOptions options) { _dependencies = dependencies; + _typeMappingSource = typeMappingSource; _options = options; } public virtual QuerySqlGenerator Create() - => new SingleStoreQuerySqlGenerator(_dependencies, _options); + => new SingleStoreQuerySqlGenerator(_dependencies, _typeMappingSource, _options); } } diff --git a/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreQueryTranslationPreprocessor.cs b/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreQueryTranslationPreprocessor.cs new file mode 100644 index 000000000..d7142646b --- /dev/null +++ b/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreQueryTranslationPreprocessor.cs @@ -0,0 +1,39 @@ +// Copyright (c) Pomelo Foundation. All rights reserved. +// Copyright (c) SingleStore Inc. All rights reserved. +// Licensed under the MIT. See LICENSE in the project root for license information. + +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Query.Internal; + +namespace EntityFrameworkCore.SingleStore.Query.ExpressionVisitors.Internal; + +public class SingleStoreQueryTranslationPreprocessor : RelationalQueryTranslationPreprocessor +{ + private readonly RelationalQueryCompilationContext _relationalQueryCompilationContext; + + public SingleStoreQueryTranslationPreprocessor( + QueryTranslationPreprocessorDependencies dependencies, + RelationalQueryTranslationPreprocessorDependencies relationalDependencies, + QueryCompilationContext queryCompilationContext) + : base(dependencies, relationalDependencies, queryCompilationContext) + { + _relationalQueryCompilationContext = (RelationalQueryCompilationContext)queryCompilationContext; + } + + /// + /// Workaround https://github.com/dotnet/efcore/issues/30386. + /// + public override Expression NormalizeQueryableMethod(Expression expression) + { + // Implementation of base (RelationalQueryTranslationPreprocessor). + expression = new RelationalQueryMetadataExtractingExpressionVisitor(_relationalQueryCompilationContext).Visit(expression); + + // Implementation of base.base (QueryTranslationPreprocessor), using `SingleStoreQueryableMethodNormalizingExpressionVisitor` instead of + // `QueryableMethodNormalizingExpressionVisitor` directly. + expression = new SingleStoreQueryableMethodNormalizingExpressionVisitor(QueryCompilationContext).Normalize(expression); + expression = ProcessQueryRoots(expression); + + return expression; + } +} diff --git a/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreQueryTranslationPreprocessorFactory.cs b/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreQueryTranslationPreprocessorFactory.cs new file mode 100644 index 000000000..81a96f488 --- /dev/null +++ b/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreQueryTranslationPreprocessorFactory.cs @@ -0,0 +1,27 @@ +// Copyright (c) Pomelo Foundation. All rights reserved. +// Copyright (c) SingleStore Inc. All rights reserved. +// Licensed under the MIT. See LICENSE in the project root for license information. + +using Microsoft.EntityFrameworkCore.Query; + +namespace EntityFrameworkCore.SingleStore.Query.ExpressionVisitors.Internal; + +public class SingleStoreQueryTranslationPreprocessorFactory : IQueryTranslationPreprocessorFactory +{ + private readonly QueryTranslationPreprocessorDependencies _dependencies; + private readonly RelationalQueryTranslationPreprocessorDependencies _relationalDependencies; + + public SingleStoreQueryTranslationPreprocessorFactory( + QueryTranslationPreprocessorDependencies dependencies, + RelationalQueryTranslationPreprocessorDependencies relationalDependencies) + { + _dependencies = dependencies; + _relationalDependencies = relationalDependencies; + } + + public virtual QueryTranslationPreprocessor Create(QueryCompilationContext queryCompilationContext) + => new SingleStoreQueryTranslationPreprocessor( + _dependencies, + _relationalDependencies, + queryCompilationContext); +} diff --git a/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreQueryableMethodNormalizingExpressionVisitor.cs b/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreQueryableMethodNormalizingExpressionVisitor.cs new file mode 100644 index 000000000..2c09739c3 --- /dev/null +++ b/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreQueryableMethodNormalizingExpressionVisitor.cs @@ -0,0 +1,94 @@ +// Copyright (c) Pomelo Foundation. All rights reserved. +// Copyright (c) SingleStore Inc. All rights reserved. +// Licensed under the MIT. See LICENSE in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Query.Internal; +using EntityFrameworkCore.SingleStore.Query.Expressions.Internal; + +namespace EntityFrameworkCore.SingleStore.Query.ExpressionVisitors.Internal; + +/// +/// Skips normalization of array[index].Property to array.Select(e => e.Property).ElementAt(index), +/// because it messes-up our JSON-Array handling in `SingleStoreSqlTranslatingExpressionVisitor`. +/// See https://github.com/dotnet/efcore/issues/30386. +/// +public class SingleStoreQueryableMethodNormalizingExpressionVisitor : QueryableMethodNormalizingExpressionVisitor +{ + public SingleStoreQueryableMethodNormalizingExpressionVisitor(QueryCompilationContext queryCompilationContext) + : base(queryCompilationContext) + { + } + + protected override Expression VisitBinary(BinaryExpression binaryExpression) + { + // Convert array[x] to array.ElementAt(x) + if (binaryExpression is + { + NodeType: ExpressionType.ArrayIndex, + Left: var source, + Right: var index + }) + { + return new SingleStoreBipolarExpression( + base.VisitBinary(binaryExpression), + binaryExpression.Update( + Visit(binaryExpression.Left), + VisitAndConvert(binaryExpression.Conversion, nameof(VisitBinary)), + Visit(binaryExpression.Right))); + + // Original (base) implementation: + // + // return VisitMethodCall( + // Expression.Call( + // ElementAtMethodInfo.MakeGenericMethod(source.Type.GetSequenceType()), source, index)); + } + + return base.VisitBinary(binaryExpression); + } + + protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression) + { + // Normalize list[x] to list.ElementAt(x) + if (methodCallExpression is + { + Method: + { + Name: "get_Item", + IsStatic: false, + DeclaringType: Type declaringType + }, + Object: Expression indexerSource, + Arguments: [var index] + } + && declaringType.GetInterface("IReadOnlyList`1") is not null) + { + return new SingleStoreBipolarExpression( + base.VisitMethodCall(methodCallExpression), + methodCallExpression.Update( + Visit(methodCallExpression.Object), + VisitArguments(methodCallExpression.Arguments))); + + IEnumerable VisitArguments(IEnumerable arguments) + { + foreach (var expression in arguments) + { + yield return VisitExtension(expression); + } + } + + // Original (base) implementation: + // + // return VisitMethodCall( + // Expression.Call( + // ElementAtMethodInfo.MakeGenericMethod(indexerSource.Type.GetSequenceType()), + // indexerSource, + // index)); + } + + return base.VisitMethodCall(methodCallExpression); + } +} diff --git a/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreSqlTranslatingExpressionVisitor.cs b/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreSqlTranslatingExpressionVisitor.cs index 097a525e4..740c762c5 100644 --- a/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.SingleStore/Query/ExpressionVisitors/Internal/SingleStoreSqlTranslatingExpressionVisitor.cs @@ -3,6 +3,7 @@ // Licensed under the MIT. See LICENSE in the project root for license information. using System; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Linq.Expressions; @@ -24,15 +25,27 @@ namespace EntityFrameworkCore.SingleStore.Query.ExpressionVisitors.Internal { public class SingleStoreSqlTranslatingExpressionVisitor : RelationalSqlTranslatingExpressionVisitor { + private readonly QueryCompilationContext _queryCompilationContext; private readonly ISingleStoreJsonPocoTranslator _jsonPocoTranslator; private readonly SingleStoreSqlExpressionFactory _sqlExpressionFactory; protected static readonly MethodInfo[] NewArrayExpressionSupportMethodInfos = Array.Empty() - .Concat(typeof(SingleStoreDbFunctionsExtensions).GetRuntimeMethods().Where(m => m.Name == nameof(SingleStoreDbFunctionsExtensions.Match))) + .Concat(typeof(SingleStoreDbFunctionsExtensions).GetRuntimeMethods().Where(m => m.Name is nameof(SingleStoreDbFunctionsExtensions.Match) + or nameof(SingleStoreDbFunctionsExtensions.IsMatch))) .Concat(typeof(string).GetRuntimeMethods().Where(m => m.Name == nameof(string.Concat))) .Where(m => m.GetParameters().Any(p => p.ParameterType.IsArray)) .ToArray(); + protected static readonly MethodInfo ElementAtMethodInfo = typeof(Enumerable) + .GetRuntimeMethods() + .Single(m => m.Name == nameof(Enumerable.ElementAt) && + m.GetParameters() + .Select( + p => p.ParameterType.IsGenericType + ? p.ParameterType.GetGenericTypeDefinition() + : p.ParameterType) + .SequenceEqual(new[] { typeof(IEnumerable<>), typeof(int) })); + public SingleStoreSqlTranslatingExpressionVisitor( RelationalSqlTranslatingExpressionVisitorDependencies dependencies, QueryCompilationContext queryCompilationContext, @@ -40,10 +53,36 @@ public SingleStoreSqlTranslatingExpressionVisitor( [CanBeNull] ISingleStoreJsonPocoTranslator jsonPocoTranslator) : base(dependencies, queryCompilationContext, queryableMethodTranslatingExpressionVisitor) { + _queryCompilationContext = queryCompilationContext; _jsonPocoTranslator = jsonPocoTranslator; _sqlExpressionFactory = (SingleStoreSqlExpressionFactory)Dependencies.SqlExpressionFactory; } + protected override Expression VisitExtension(Expression extensionExpression) + => extensionExpression switch + { + SingleStoreBipolarExpression bipolarExpression => VisitSingleStoreBipolarExpression(bipolarExpression), + _ => base.VisitExtension(extensionExpression) + }; + + private Expression VisitSingleStoreBipolarExpression(SingleStoreBipolarExpression bipolarExpression) + { + var defaultExpression = Visit(bipolarExpression.DefaultExpression) ?? QueryCompilationContext.NotTranslatedExpression; + var alternativeExpression = Visit(bipolarExpression.AlternativeExpression) ?? QueryCompilationContext.NotTranslatedExpression; + + return defaultExpression != QueryCompilationContext.NotTranslatedExpression + // ? alternativeExpression != QueryCompilationContext.NotTranslatedExpression + // // ? new SingleStoreBipolarSqlExpression( + // // (SqlExpression)defaultExpression, + // // (SqlExpression)alternativeExpression) + // ? QueryCompilationContext.NotTranslatedExpression + // : (SqlExpression)defaultExpression + ? (SqlExpression)defaultExpression + : alternativeExpression != QueryCompilationContext.NotTranslatedExpression + ? (SqlExpression)alternativeExpression + : QueryCompilationContext.NotTranslatedExpression; + } + /// protected override Expression VisitUnary(UnaryExpression unaryExpression) { @@ -116,26 +155,7 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression) if (binaryExpression.Left.Type == typeof(byte[])) { - if (Visit(binaryExpression.Left) is SqlExpression leftSql && - Visit(binaryExpression.Right) is SqlExpression rightSql) - { - return _sqlExpressionFactory.NullableFunction( - "ASCII", - new[] - { - _sqlExpressionFactory.NullableFunction( - "SUBSTRING", - new[] - { - leftSql, Dependencies.SqlExpressionFactory.Add( - Dependencies.SqlExpressionFactory.ApplyDefaultTypeMapping(rightSql), - Dependencies.SqlExpressionFactory.Constant(1)), - Dependencies.SqlExpressionFactory.Constant(1) - }, - typeof(byte[])) - }, - typeof(byte)); - } + return TranslateByteArrayElementAccess(sqlLeft, sqlRight); } // Try translating ArrayIndex inside json column @@ -284,6 +304,27 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression) return base.VisitBinary(binaryExpression); } + private Expression TranslateByteArrayElementAccess(Expression array, Expression index) + => Visit(array) is SqlExpression leftSql && + Visit(index) is SqlExpression rightSql + ? _sqlExpressionFactory.NullableFunction( + "ASCII", + new[] + { + _sqlExpressionFactory.NullableFunction( + "SUBSTRING", + new[] + { + leftSql, Dependencies.SqlExpressionFactory.Add( + Dependencies.SqlExpressionFactory.ApplyDefaultTypeMapping(rightSql), + Dependencies.SqlExpressionFactory.Constant(1)), + Dependencies.SqlExpressionFactory.Constant(1) + }, + typeof(byte[])) + }, + typeof(byte)) + : QueryCompilationContext.NotTranslatedExpression; + protected virtual Expression VisitMethodCallNewArray(NewArrayExpression newArrayExpression) { // Needed for SingleStoreDbFunctionsExtensions.Match() and String.Concat() translation. @@ -313,6 +354,15 @@ protected virtual Expression VisitMethodCallNewArray(NewArrayExpression newArray protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression) { + if (methodCallExpression.Method.IsGenericMethod + && methodCallExpression.Method.GetGenericMethodDefinition() == ElementAtMethodInfo + && methodCallExpression.Arguments[0].Type == typeof(byte[])) + { + return TranslateByteArrayElementAccess( + methodCallExpression.Arguments[0], + methodCallExpression.Arguments[1]); + } + if (NewArrayExpressionSupportMethodInfos.Contains(methodCallExpression.Method)) { var arguments = new Expression[methodCallExpression.Arguments.Count]; @@ -338,7 +388,8 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp methodCallExpression = methodCallExpression.Update(methodCallExpression.Object, arguments); } - var result = base.VisitMethodCall(methodCallExpression); + var result = CallBaseVisitMethodCall(methodCallExpression); + if (result == QueryCompilationContext.NotTranslatedExpression && SingleStoreStringComparisonMethodTranslator.StringComparisonMethodInfos.Any(m => m == methodCallExpression.Method)) { @@ -364,6 +415,36 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp return result; } + /// + /// EF Core does not forward the current QueryCompilationContext to IMethodCallTranslator implementations. + /// Our SingleStoreMethodCallTranslatorProvider and SingleStoreQueryCompilationContextMethodTranslator implementations take care of that. + /// + private Expression CallBaseVisitMethodCall(MethodCallExpression methodCallExpression) + { + var mySqlMethodCallTranslatorProvider = (SingleStoreMethodCallTranslatorProvider)Dependencies.MethodCallTranslatorProvider; + + if (mySqlMethodCallTranslatorProvider.QueryCompilationContext is null) + { + mySqlMethodCallTranslatorProvider.QueryCompilationContext = _queryCompilationContext; + + try + { + return base.VisitMethodCall(methodCallExpression); + } + finally + { + mySqlMethodCallTranslatorProvider.QueryCompilationContext = null; + } + } + + if (mySqlMethodCallTranslatorProvider.QueryCompilationContext == _queryCompilationContext) + { + return base.VisitMethodCall(methodCallExpression); + } + + throw new UnreachableException(); + } + protected virtual void ResetTranslationErrorDetails() { // When we try translating an expression and later decide that we want to discard the result, we need to remove any translation diff --git a/src/EFCore.SingleStore/Query/Expressions/Internal/SingleStoreBipolarExpression.cs b/src/EFCore.SingleStore/Query/Expressions/Internal/SingleStoreBipolarExpression.cs new file mode 100644 index 000000000..9fc598f23 --- /dev/null +++ b/src/EFCore.SingleStore/Query/Expressions/Internal/SingleStoreBipolarExpression.cs @@ -0,0 +1,81 @@ +// Copyright (c) Pomelo Foundation. All rights reserved. +// Copyright (c) SingleStore Inc. All rights reserved. +// Licensed under the MIT. See LICENSE in the project root for license information. + +using System; +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace EntityFrameworkCore.SingleStore.Query.Expressions.Internal; + +public class SingleStoreBipolarExpression : Expression +{ + public SingleStoreBipolarExpression( + Expression defaultExpression, + Expression alternativeExpression) + { + Check.NotNull(defaultExpression, nameof(defaultExpression)); + Check.NotNull(alternativeExpression, nameof(alternativeExpression)); + + DefaultExpression = defaultExpression; + AlternativeExpression = alternativeExpression; + } + + public virtual Expression DefaultExpression { get; } + public virtual Expression AlternativeExpression { get; } + + public override ExpressionType NodeType + => ExpressionType.UnaryPlus; + + public override Type Type + => DefaultExpression.Type; + + protected override Expression VisitChildren(ExpressionVisitor visitor) + { + var defaultExpression = visitor.Visit(DefaultExpression); + var alternativeExpression = visitor.Visit(AlternativeExpression); + + return Update(defaultExpression, alternativeExpression); + } + + public virtual SingleStoreBipolarExpression Update(Expression defaultExpression, Expression alternativeExpression) + => defaultExpression != DefaultExpression || alternativeExpression != AlternativeExpression + ? new SingleStoreBipolarExpression(defaultExpression, alternativeExpression) + : this; + + public override string ToString() + { + var expressionPrinter = new ExpressionPrinter(); + + expressionPrinter.AppendLine("("); + + using (expressionPrinter.Indent()) + { + expressionPrinter.Append("Default: "); + expressionPrinter.Visit(DefaultExpression); + expressionPrinter.AppendLine(); + + expressionPrinter.Append("Alternative: "); + expressionPrinter.Visit(AlternativeExpression); + expressionPrinter.AppendLine(); + } + + expressionPrinter.Append(")"); + + return expressionPrinter.ToString(); + } + + public override bool Equals(object obj) + => obj != null + && (ReferenceEquals(this, obj) + || obj is SingleStoreBipolarExpression bipolarExpression + && Equals(bipolarExpression)); + + private bool Equals(SingleStoreBipolarExpression bipolarExpression) + => base.Equals(bipolarExpression) + && DefaultExpression.Equals(bipolarExpression.DefaultExpression) + && AlternativeExpression.Equals(bipolarExpression.AlternativeExpression); + + public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), DefaultExpression, AlternativeExpression); +} diff --git a/src/EFCore.SingleStore/Query/Expressions/Internal/SingleStoreInlinedParameterExpression.cs b/src/EFCore.SingleStore/Query/Expressions/Internal/SingleStoreInlinedParameterExpression.cs new file mode 100644 index 000000000..bf4291058 --- /dev/null +++ b/src/EFCore.SingleStore/Query/Expressions/Internal/SingleStoreInlinedParameterExpression.cs @@ -0,0 +1,62 @@ +// Copyright (c) Pomelo Foundation. All rights reserved. +// Copyright (c) SingleStore Inc. All rights reserved. +// Licensed under the MIT. See LICENSE in the project root for license information. + +using System; +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace EntityFrameworkCore.SingleStore.Query.Expressions.Internal; + +public class SingleStoreInlinedParameterExpression : SqlExpression +{ + public SingleStoreInlinedParameterExpression( + SqlParameterExpression parameterExpression, + SqlConstantExpression valueExpression) + : base(parameterExpression.Type, parameterExpression.TypeMapping) + { + Check.NotNull(parameterExpression, nameof(parameterExpression)); + + ParameterExpression = parameterExpression; + ValueExpression = valueExpression; + } + + public virtual Expression ParameterExpression { get; } + public virtual SqlConstantExpression ValueExpression { get; } + + protected override Expression VisitChildren(ExpressionVisitor visitor) + { + var parameterExpression = (SqlParameterExpression)visitor.Visit(ParameterExpression); + var valueExpression = (SqlConstantExpression)visitor.Visit(ValueExpression); + + return Update(parameterExpression, valueExpression); + } + + protected override void Print(ExpressionPrinter expressionPrinter) + { + expressionPrinter.Visit(ValueExpression); + expressionPrinter.Append(" (<<== "); + expressionPrinter.Visit(ParameterExpression); + expressionPrinter.Append(")"); + } + + public virtual SingleStoreInlinedParameterExpression Update(SqlParameterExpression parameterExpression, SqlConstantExpression valueExpression) + => parameterExpression != ParameterExpression || valueExpression != ValueExpression + ? new SingleStoreInlinedParameterExpression(parameterExpression, valueExpression) + : this; + + public override bool Equals(object obj) + => obj != null + && (ReferenceEquals(this, obj) + || obj is SingleStoreInlinedParameterExpression inlinedParameterExpression + && Equals(inlinedParameterExpression)); + + private bool Equals(SingleStoreInlinedParameterExpression inlinedParameterExpression) + => base.Equals(inlinedParameterExpression) + && ParameterExpression.Equals(inlinedParameterExpression.ParameterExpression) + && ValueExpression.Equals(inlinedParameterExpression.ValueExpression); + + public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), ParameterExpression, ValueExpression); +} diff --git a/src/EFCore.SingleStore/Query/Expressions/Internal/SingleStoreMatchExpression.cs b/src/EFCore.SingleStore/Query/Expressions/Internal/SingleStoreMatchExpression.cs index 3c628871f..cd49b836b 100644 --- a/src/EFCore.SingleStore/Query/Expressions/Internal/SingleStoreMatchExpression.cs +++ b/src/EFCore.SingleStore/Query/Expressions/Internal/SingleStoreMatchExpression.cs @@ -19,7 +19,7 @@ public SingleStoreMatchExpression( SqlExpression match, SqlExpression against, RelationalTypeMapping typeMapping) - : base(typeof(bool), typeMapping) + : base(typeof(double), typeMapping) { Check.NotNull(match, nameof(match)); Check.NotNull(against, nameof(against)); diff --git a/src/EFCore.SingleStore/Query/Internal/SingleStoreDateTimeMethodTranslator.cs b/src/EFCore.SingleStore/Query/Internal/SingleStoreDateTimeMethodTranslator.cs index b6e3906d9..2f8cf0de7 100644 --- a/src/EFCore.SingleStore/Query/Internal/SingleStoreDateTimeMethodTranslator.cs +++ b/src/EFCore.SingleStore/Query/Internal/SingleStoreDateTimeMethodTranslator.cs @@ -23,19 +23,25 @@ public class SingleStoreDateTimeMethodTranslator : IMethodCallTranslator { typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddHours), new[] { typeof(double) }), "hour" }, { typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddMinutes), new[] { typeof(double) }), "minute" }, { typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddSeconds), new[] { typeof(double) }), "second" }, - { typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddMilliseconds), new[] { typeof(double) }), "microsecond" }, + { typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddMilliseconds), new[] { typeof(double) }), "millisecond" }, { typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddYears), new[] { typeof(int) }), "year" }, { typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddMonths), new[] { typeof(int) }), "month" }, { typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddDays), new[] { typeof(double) }), "day" }, { typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddHours), new[] { typeof(double) }), "hour" }, { typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddMinutes), new[] { typeof(double) }), "minute" }, { typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddSeconds), new[] { typeof(double) }), "second" }, - { typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddMilliseconds), new[] { typeof(double) }), "microsecond" }, + { typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddMilliseconds), new[] { typeof(double) }), "millisecond" }, { typeof(DateOnly).GetRuntimeMethod(nameof(DateOnly.AddYears), new[] { typeof(int) }), "year" }, { typeof(DateOnly).GetRuntimeMethod(nameof(DateOnly.AddMonths), new[] { typeof(int) }), "month" }, { typeof(DateOnly).GetRuntimeMethod(nameof(DateOnly.AddDays), new[] { typeof(int) }), "day" }, }; + private static readonly Dictionary _methodInfoDateDiffMapping = new() + { + { typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.ToUnixTimeSeconds), Type.EmptyTypes)!, "second" }, + { typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.ToUnixTimeMilliseconds), Type.EmptyTypes)!, "millisecond" } + }; + private static readonly MethodInfo _timeOnlyAddTimeSpanMethod = typeof(TimeOnly).GetRuntimeMethod(nameof(TimeOnly.Add), new[] { typeof(TimeSpan) })!; private static readonly MethodInfo _timeOnlyAddHoursMethod = typeof(TimeOnly).GetRuntimeMethod(nameof(TimeOnly.AddHours), new[] {typeof(double)})!; private static readonly MethodInfo _timeOnlyAddMinutesMethod = typeof(TimeOnly).GetRuntimeMethod(nameof(TimeOnly.AddMinutes), new[] {typeof(double)})!; @@ -72,12 +78,15 @@ public virtual SqlExpression Translate( new SqlExpression[] { _sqlExpressionFactory.Fragment("INTERVAL"), - datePart.Equals("microsecond") + datePart.Equals("millisecond") ? _sqlExpressionFactory.Multiply( _sqlExpressionFactory.Constant(1000), _sqlExpressionFactory.Convert(arguments[0], typeof(int))) : _sqlExpressionFactory.Convert(arguments[0], typeof(int)), - _sqlExpressionFactory.Fragment(datePart) + _sqlExpressionFactory.Fragment( + datePart == "millisecond" + ? "microsecond" + : datePart) }, " ", typeof(string)) @@ -88,6 +97,35 @@ public virtual SqlExpression Translate( new[] {true, false}); } + if (method.DeclaringType == typeof(DateTimeOffset) && + instance is not null) + { + if (_methodInfoDateDiffMapping.TryGetValue(method, out var timePart)) + { + SqlExpression expression = _sqlExpressionFactory.NullableFunction( + "TIMESTAMPDIFF", + new[] + { + _sqlExpressionFactory.Fragment(timePart == "millisecond" ? "microsecond" : timePart), + _sqlExpressionFactory.Constant(DateTimeOffset.UnixEpoch, instance!.TypeMapping), + instance + }, + typeof(long), + typeMapping: null, + onlyNullWhenAnyNullPropagatingArgumentIsNull: true, + argumentsPropagateNullability: new[] { false, true, true }); + + if (timePart == "millisecond") + { + expression = _sqlExpressionFactory.SingleStoreIntegerDivide( + expression, + _sqlExpressionFactory.Constant(1_000)); + } + + return expression; + } + } + if (method.DeclaringType == typeof(TimeOnly)) { if (method == _timeOnlyAddTimeSpanMethod) diff --git a/src/EFCore.SingleStore/Query/Internal/SingleStoreMathMethodTranslator.cs b/src/EFCore.SingleStore/Query/Internal/SingleStoreMathMethodTranslator.cs index b85a9b4d6..70aadf0c4 100644 --- a/src/EFCore.SingleStore/Query/Internal/SingleStoreMathMethodTranslator.cs +++ b/src/EFCore.SingleStore/Query/Internal/SingleStoreMathMethodTranslator.cs @@ -43,6 +43,11 @@ public class SingleStoreMathMethodTranslator : IMethodCallTranslator { typeof(Math).GetRuntimeMethod(nameof(Math.Cos), new[] { typeof(double) }), ("COS", true) }, { typeof(MathF).GetRuntimeMethod(nameof(MathF.Cos), new[] { typeof(float) }), ("COS", true) }, + { typeof(double).GetRuntimeMethod(nameof(double.DegreesToRadians), new[] { typeof(double) })!, ("RADIANS", true) }, + { typeof(float).GetRuntimeMethod(nameof(float.DegreesToRadians), new[] { typeof(float) })!, ("RADIANS", true) }, + { typeof(double).GetRuntimeMethod(nameof(double.RadiansToDegrees), new[] { typeof(double) })!, ("DEGREES", true) }, + { typeof(float).GetRuntimeMethod(nameof(float.RadiansToDegrees), new[] { typeof(float) })!, ("DEGREES", true) }, + { typeof(Math).GetRuntimeMethod(nameof(Math.Exp), new[] { typeof(double) }), ("EXP", true) }, { typeof(MathF).GetRuntimeMethod(nameof(MathF.Exp), new[] { typeof(float) }), ("EXP", true) }, diff --git a/src/EFCore.SingleStore/Query/Internal/SingleStoreMethodCallTranslatorProvider.cs b/src/EFCore.SingleStore/Query/Internal/SingleStoreMethodCallTranslatorProvider.cs index 8504d7a0d..477800d3b 100644 --- a/src/EFCore.SingleStore/Query/Internal/SingleStoreMethodCallTranslatorProvider.cs +++ b/src/EFCore.SingleStore/Query/Internal/SingleStoreMethodCallTranslatorProvider.cs @@ -2,8 +2,15 @@ // Copyright (c) SingleStore Inc. All rights reserved. // Licensed under the MIT. See LICENSE in the project root for license information. +using System; +using System.Collections.Generic; +using System.Reflection; using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using EntityFrameworkCore.SingleStore.Infrastructure.Internal; using EntityFrameworkCore.SingleStore.Query.ExpressionTranslators.Internal; using EntityFrameworkCore.SingleStore.Storage.Internal; @@ -32,9 +39,22 @@ public SingleStoreMethodCallTranslatorProvider( new SingleStoreNewGuidTranslator(sqlExpressionFactory), new SingleStoreObjectToStringTranslator(sqlExpressionFactory), new SingleStoreRegexIsMatchTranslator(sqlExpressionFactory), - new SingleStoreStringComparisonMethodTranslator(sqlExpressionFactory, options), - new SingleStoreStringMethodTranslator(sqlExpressionFactory, relationalTypeMappingSource, options), + new SingleStoreStringComparisonMethodTranslator(sqlExpressionFactory, () => QueryCompilationContext, options), + new SingleStoreStringMethodTranslator(sqlExpressionFactory, relationalTypeMappingSource, () => QueryCompilationContext, options), }); } + + + public virtual QueryCompilationContext QueryCompilationContext { get; set; } + + public override SqlExpression Translate( + IModel model, + SqlExpression instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + => QueryCompilationContext is not null + ? base.Translate(model, instance, method, arguments, logger) + : throw new InvalidOperationException(); } } diff --git a/src/EFCore.SingleStore/Query/Internal/SingleStoreParameterBasedSqlProcessor.cs b/src/EFCore.SingleStore/Query/Internal/SingleStoreParameterBasedSqlProcessor.cs index d138f5b81..e5373bc35 100644 --- a/src/EFCore.SingleStore/Query/Internal/SingleStoreParameterBasedSqlProcessor.cs +++ b/src/EFCore.SingleStore/Query/Internal/SingleStoreParameterBasedSqlProcessor.cs @@ -17,7 +17,6 @@ namespace EntityFrameworkCore.SingleStore.Query.Internal public class SingleStoreParameterBasedSqlProcessor : RelationalParameterBasedSqlProcessor { private readonly ISingleStoreOptions _options; - private readonly SingleStoreSqlExpressionFactory _sqlExpressionFactory; public SingleStoreParameterBasedSqlProcessor( RelationalParameterBasedSqlProcessorDependencies dependencies, @@ -25,7 +24,6 @@ public SingleStoreParameterBasedSqlProcessor( ISingleStoreOptions options) : base(dependencies, useRelationalNulls) { - _sqlExpressionFactory = (SingleStoreSqlExpressionFactory)Dependencies.SqlExpressionFactory; _options = options; } @@ -49,7 +47,14 @@ public override Expression Optimize( queryExpression = new SingleStoreBoolOptimizingExpressionVisitor(Dependencies.SqlExpressionFactory).Visit(queryExpression); } - queryExpression = new SingleStoreHavingExpressionVisitor(_sqlExpressionFactory).Visit(queryExpression); + queryExpression = new SingleStoreHavingExpressionVisitor((SingleStoreSqlExpressionFactory)Dependencies.SqlExpressionFactory).Visit(queryExpression); + + queryExpression = new SingleStoreParameterInliningExpressionVisitor( + Dependencies.TypeMappingSource, + Dependencies.SqlExpressionFactory, + _options).Process(queryExpression, parametersValues, out var canCache3); + + canCache &= canCache3; // Run the compatibility checks as late in the query pipeline (before the actual SQL translation happens) as reasonable. queryExpression = new SingleStoreCompatibilityExpressionVisitor(_options).Visit(queryExpression); diff --git a/src/EFCore.SingleStore/Query/Internal/SingleStoreQueryCompilationContextMethodTranslator.cs b/src/EFCore.SingleStore/Query/Internal/SingleStoreQueryCompilationContextMethodTranslator.cs new file mode 100644 index 000000000..4cdef492f --- /dev/null +++ b/src/EFCore.SingleStore/Query/Internal/SingleStoreQueryCompilationContextMethodTranslator.cs @@ -0,0 +1,36 @@ +// Copyright (c) Pomelo Foundation. All rights reserved. +// Copyright (c) SingleStore Inc. All rights reserved. +// Licensed under the MIT. See LICENSE in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Reflection; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; + +namespace EntityFrameworkCore.SingleStore.Query.Internal; + +public abstract class SingleStoreQueryCompilationContextMethodTranslator : IMethodCallTranslator +{ + private readonly Func _queryCompilationContextResolver; + + protected SingleStoreQueryCompilationContextMethodTranslator(Func queryCompilationContextResolver) + { + _queryCompilationContextResolver = queryCompilationContextResolver; + } + + public virtual SqlExpression Translate( + SqlExpression instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + => Translate(instance, method, arguments, _queryCompilationContextResolver() ?? throw new InvalidOperationException()); + + public abstract SqlExpression Translate( + SqlExpression instance, + MethodInfo method, + IReadOnlyList arguments, + QueryCompilationContext queryCompilationContext); +} diff --git a/src/EFCore.SingleStore/Query/Internal/SingleStoreQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.SingleStore/Query/Internal/SingleStoreQueryableMethodTranslatingExpressionVisitor.cs index df0e96635..e9d829087 100644 --- a/src/EFCore.SingleStore/Query/Internal/SingleStoreQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.SingleStore/Query/Internal/SingleStoreQueryableMethodTranslatingExpressionVisitor.cs @@ -2,30 +2,83 @@ // Copyright (c) SingleStore Inc. All rights reserved. // Licensed under the MIT. See LICENSE in the project root for license information. +using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Utilities; +using EntityFrameworkCore.SingleStore.Infrastructure.Internal; +using EntityFrameworkCore.SingleStore.Query.ExpressionTranslators.Internal; +using EntityFrameworkCore.SingleStore.Storage.Internal; namespace EntityFrameworkCore.SingleStore.Query.Internal; public class SingleStoreQueryableMethodTranslatingExpressionVisitor : RelationalQueryableMethodTranslatingExpressionVisitor { + private readonly ISingleStoreOptions _options; + private readonly SingleStoreSqlExpressionFactory _sqlExpressionFactory; + private readonly IRelationalTypeMappingSource _typeMappingSource; + public SingleStoreQueryableMethodTranslatingExpressionVisitor( QueryableMethodTranslatingExpressionVisitorDependencies dependencies, RelationalQueryableMethodTranslatingExpressionVisitorDependencies relationalDependencies, - QueryCompilationContext queryCompilationContext) + QueryCompilationContext queryCompilationContext, + ISingleStoreOptions options) : base(dependencies, relationalDependencies, queryCompilationContext) { + _sqlExpressionFactory = (SingleStoreSqlExpressionFactory)relationalDependencies.SqlExpressionFactory; + _typeMappingSource = relationalDependencies.TypeMappingSource; + _options = options; + } + + protected SingleStoreQueryableMethodTranslatingExpressionVisitor( + SingleStoreQueryableMethodTranslatingExpressionVisitor parentVisitor) + : base(parentVisitor) + { + _sqlExpressionFactory = parentVisitor._sqlExpressionFactory; + _typeMappingSource = parentVisitor._typeMappingSource; + _options = parentVisitor._options; + } + + protected override QueryableMethodTranslatingExpressionVisitor CreateSubqueryVisitor() + => new SingleStoreQueryableMethodTranslatingExpressionVisitor(this); + + protected override bool IsNaturallyOrdered(SelectExpression selectExpression) + { + return selectExpression is + { + Tables: [var mainTable, ..], + Orderings: + [ + { + Expression: ColumnExpression { Name: "key", Table: var orderingTable } orderingColumn, + IsAscending: true + } + ] + } + && orderingTable == mainTable + && IsJsonEachKeyColumn(orderingColumn); + + bool IsJsonEachKeyColumn(ColumnExpression orderingColumn) + => orderingColumn.Table is SingleStoreJsonTableExpression + || (orderingColumn.Table is SelectExpression subquery + && subquery.Projection.FirstOrDefault(p => p.Alias == "key")?.Expression is ColumnExpression projectedColumn + && IsJsonEachKeyColumn(projectedColumn)); } protected override bool IsValidSelectExpressionForExecuteDelete( SelectExpression selectExpression, - EntityShaperExpression entityShaperExpression, + StructuralTypeShaperExpression shaper, [NotNullWhen(true)] out TableExpression tableExpression) { if (selectExpression.Offset == null - && (!selectExpression.IsDistinct || entityShaperExpression.EntityType.FindPrimaryKey() != null) && selectExpression.GroupBy.Count == 0 && selectExpression.Having == null && (selectExpression.Tables.Count == 1 || selectExpression.Orderings.Count == 0)) @@ -37,9 +90,9 @@ protected override bool IsValidSelectExpressionForExecuteDelete( } else { - var projectionBindingExpression = (ProjectionBindingExpression)entityShaperExpression.ValueBufferExpression; - var entityProjectionExpression = (EntityProjectionExpression)selectExpression.GetProjection(projectionBindingExpression); - var column = entityProjectionExpression.BindProperty(entityShaperExpression.EntityType.GetProperties().First()); + var projectionBindingExpression = (ProjectionBindingExpression)shaper.ValueBufferExpression; + var entityProjectionExpression = (StructuralTypeProjectionExpression)selectExpression.GetProjection(projectionBindingExpression); + var column = entityProjectionExpression.BindProperty(shaper.StructuralType.GetProperties().First()); table = column.Table; if (table is JoinExpressionBase joinExpressionBase) { @@ -60,15 +113,17 @@ protected override bool IsValidSelectExpressionForExecuteDelete( protected override bool IsValidSelectExpressionForExecuteUpdate( SelectExpression selectExpression, - EntityShaperExpression entityShaperExpression, + TableExpressionBase targetTable, [NotNullWhen(true)] out TableExpression tableExpression) { - if (selectExpression.Offset == null - // If entity type has primary key then Distinct is no-op - && (!selectExpression.IsDistinct || entityShaperExpression.EntityType.FindPrimaryKey() != null) - && selectExpression.GroupBy.Count == 0 - && selectExpression.Having == null - && selectExpression.Orderings.Count == 0) + if (selectExpression is + { + Offset: null, + IsDistinct: false, + GroupBy: [], + Having: null, + Orderings: [] + }) { TableExpressionBase table; if (selectExpression.Tables.Count == 1) @@ -77,11 +132,11 @@ protected override bool IsValidSelectExpressionForExecuteUpdate( } else { - var projectionBindingExpression = (ProjectionBindingExpression)entityShaperExpression.ValueBufferExpression; - var entityProjectionExpression = (EntityProjectionExpression)selectExpression.GetProjection(projectionBindingExpression); - var column = entityProjectionExpression.BindProperty(entityShaperExpression.EntityType.GetProperties().First()); - table = column.Table; - if (table is JoinExpressionBase joinExpressionBase) + table = targetTable; + + if (selectExpression.Tables.Count > 1 && + table is JoinExpressionBase joinExpressionBase) + { table = joinExpressionBase.Table; } @@ -97,4 +152,355 @@ protected override bool IsValidSelectExpressionForExecuteUpdate( tableExpression = null; return false; } + + protected override ShapedQueryExpression TranslateAny(ShapedQueryExpression source, LambdaExpression predicate) + { + // Simplify x.Array.Any() => JSON_LENGTH(x.Array) > 0 instead of WHERE EXISTS (SELECT 1 FROM JSON_TABLE(x.Array)) + if (predicate is null + && source.QueryExpression is SelectExpression + { + Tables: [TableValuedFunctionExpression { Name: "JSON_TABLE", Schema: null, IsBuiltIn: true, Arguments: [var array] }], + GroupBy: [], + Having: null, + IsDistinct: false, + Limit: null, + Offset: null + }) + { + var translation = + _sqlExpressionFactory.GreaterThan( + _sqlExpressionFactory.NullableFunction( + "JSON_LENGTH", + new[] { array }, + typeof(int)), + _sqlExpressionFactory.Constant(0)); + + return source.UpdateQueryExpression(_sqlExpressionFactory.Select(translation)); + } + + return base.TranslateAny(source, predicate); + } + + protected override ShapedQueryExpression TranslateElementAtOrDefault( + ShapedQueryExpression source, + Expression index, + bool returnDefault) + { + if (!returnDefault + && source.QueryExpression is SelectExpression + { + Tables: + [ + SingleStoreJsonTableExpression + { + Name: "JSON_TABLE", Schema: null, IsBuiltIn: true, JsonExpression: var jsonArrayColumn + } jsonEachExpression + ], + GroupBy: [], + Having: null, + IsDistinct: false, + Orderings: [{ Expression: ColumnExpression { Name: "key" } orderingColumn, IsAscending: true }], + Limit: null, + Offset: null + } selectExpression + && orderingColumn.Table == jsonEachExpression + && TranslateExpression(index) is { } translatedIndex) + { + // Index on JSON array + + // Extract the column projected out of the source, and simplify the subquery to a simple JsonScalarExpression + var shaperExpression = source.ShaperExpression; + if (shaperExpression is UnaryExpression { NodeType: ExpressionType.Convert } unaryExpression + && unaryExpression.Operand.Type.IsNullableType() + && unaryExpression.Operand.Type.UnwrapNullableType() == unaryExpression.Type) + { + shaperExpression = unaryExpression.Operand; + } + + if (shaperExpression is ProjectionBindingExpression projectionBindingExpression + && selectExpression.GetProjection(projectionBindingExpression) is ColumnExpression projectionColumn) + { + SqlExpression translation = new JsonScalarExpression( + jsonArrayColumn, + new[] { new PathSegment(translatedIndex) }, + projectionColumn.Type, + projectionColumn.TypeMapping, + projectionColumn.IsNullable); + + // If we have a type mapping (i.e. translating over a column rather than a parameter), apply any necessary server-side + // conversions. + if (projectionColumn.TypeMapping is not null) + { + translation = ApplyJsonSqlConversion( + translation, _sqlExpressionFactory, projectionColumn.TypeMapping, projectionColumn.IsNullable); + } + + return source.UpdateQueryExpression(_sqlExpressionFactory.Select(translation)); + } + } + + return base.TranslateElementAtOrDefault(source, index, returnDefault); + } + + // TODO: Implement for EF Core 7 JSON support. + protected override ShapedQueryExpression TransformJsonQueryToTable(JsonQueryExpression jsonQueryExpression) + { + return base.TransformJsonQueryToTable(jsonQueryExpression); + } + + protected override ShapedQueryExpression TranslatePrimitiveCollection(SqlExpression sqlExpression, IProperty property, string tableAlias) + { + if (!_options.PrimitiveCollectionsSupport) + { + AddTranslationErrorDetails("Primitive collections support has not been enabled."); + return null; + } + + // if (!_options.ServerVersion.Supports.JsonTableImplementationUsesImplicitLateralJoin && + // sqlExpression is ColumnExpression) + // { + // // MariaDB will just return wrong results if the column of an outer table is being referenced. + // return null; + // } + + // Generate the JSON_TABLE() function expression, and wrap it in a SelectExpression. + + // Note that where the elementTypeMapping is known (i.e. collection columns), we immediately generate JSON_TABLE() with a COLUMNS clause + // (i.e. with a columnInfo), which determines the type conversion to apply to the JSON elements coming out. + // For parameter collections, the element type mapping will only be inferred and applied later (see + // SingleStoreInferredTypeMappingApplier below), at which point the we'll apply it to add the COLUMNS clause. + var elementTypeMapping = (RelationalTypeMapping)sqlExpression.TypeMapping?.ElementTypeMapping; + + var jsonTableExpression = new SingleStoreJsonTableExpression( + tableAlias, + sqlExpression, + new[] { new PathSegment(_sqlExpressionFactory.Constant("*", RelationalTypeMapping.NullMapping)) }, + elementTypeMapping is not null + ? new[] + { + new SingleStoreJsonTableExpression.ColumnInfo + { + Name = "value", + TypeMapping = elementTypeMapping, + Path = new[] { new PathSegment(_sqlExpressionFactory.Constant(0, _typeMappingSource.FindMapping(typeof(int)))) }, + } + } + : null); + + var elementClrType = sqlExpression.Type.GetSequenceType(); + + // If this is a collection property, get the element's nullability out of metadata. Otherwise, this is a parameter property, in + // which case we only have the CLR type (note that we cannot produce different SQLs based on the nullability of an *element* in + // a parameter collection - our caching mechanism only supports varying by the nullability of the parameter itself (i.e. the + // collection). + // TODO: if property is non-null, GetElementType() should never be null, but we have #31469 for shadow properties + var isElementNullable = property?.GetElementType() is null + ? elementClrType.IsNullableType() + : property.GetElementType()!.IsNullable; + +#pragma warning disable EF1001 // Internal EF Core API usage. + var selectExpression = new SelectExpression( + jsonTableExpression, + columnName: "value", + columnType: elementClrType, + columnTypeMapping: elementTypeMapping, + isElementNullable, + identifierColumnName: "key", + identifierColumnType: typeof(uint), + identifierColumnTypeMapping: _typeMappingSource.FindMapping(typeof(uint))); +#pragma warning restore EF1001 // Internal EF Core API usage. + + // JSON_TABLE() doesn't guarantee the ordering of the elements coming out; when using JSON_TABLE() without COLUMNS, a [key] column is returned + // with the JSON array's ordering, which we can ORDER BY; this option doesn't exist with JSON_TABLE() with COLUMNS, unfortunately. + // However, JSON_TABLE() with COLUMNS has better performance, and also applies JSON-specific conversions we cannot be done otherwise + // (e.g. JSON_TABLE() with COLUMNS does base64 decoding for VARBINARY). + // Here we generate JSON_TABLE() with COLUMNS, but also add an ordering by [key] - this is a temporary invalid representation. + // In MySqlQueryTranslationPostprocessor, we'll post-process the expression; if the ORDER BY was stripped (e.g. because of + // IN, EXISTS or a set operation), we'll just leave the JSON_TABLE() with COLUMNS. If not, we'll convert the JSON_TABLE() with COLUMNS to an + // JSON_TABLE() without COLUMNS. + // Note that the JSON_TABLE() 'key' column is an nvarchar - we convert it to an int before sorting. + selectExpression.AppendOrdering( + new OrderingExpression( + selectExpression.CreateColumnExpression( + jsonTableExpression, + "key", + typeof(uint), + typeMapping: _typeMappingSource.FindMapping(typeof(uint)), + columnNullable: false), + ascending: true)); + + var shaperExpression = (Expression)new ProjectionBindingExpression(selectExpression, new ProjectionMember(), elementClrType.MakeNullable()); + if (shaperExpression.Type != elementClrType) + { + Check.DebugAssert( + elementClrType.MakeNullable() == shaperExpression.Type, + "expression.Type must be nullable of targetType"); + + shaperExpression = Expression.Convert(shaperExpression, elementClrType); + } + + return new ShapedQueryExpression(selectExpression, shaperExpression); + } + + protected override Expression ApplyInferredTypeMappings( + Expression expression, + IReadOnlyDictionary<(TableExpressionBase, string), RelationalTypeMapping> inferredTypeMappings) + => new SingleStoreInferredTypeMappingApplier( + RelationalDependencies.Model, _typeMappingSource, _sqlExpressionFactory, inferredTypeMappings).Visit(expression); + + /// + /// Wraps the given expression with any SQL logic necessary to convert a value coming out of a JSON document into the relational value + /// represented by the given type mapping. + /// + private static SqlExpression ApplyJsonSqlConversion( + SqlExpression expression, + ISqlExpressionFactory sqlExpressionFactory, + RelationalTypeMapping typeMapping, + bool isNullable) + => typeMapping switch + { + // TODO: EF Core 8 - Decide between UNHEX() and FROM_BASE64(). + ByteArrayTypeMapping => sqlExpressionFactory.Function("FROM_BASE64", new[] { expression }, isNullable, new[] { true }, typeof(byte[]), typeMapping), + _ => expression + }; + + protected class SingleStoreInferredTypeMappingApplier : RelationalInferredTypeMappingApplier + { + private readonly IRelationalTypeMappingSource _typeMappingSource; + private readonly ISqlExpressionFactory _sqlExpressionFactory; + private Dictionary _currentSelectInferredTypeMappings; + + /// + /// 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 SingleStoreInferredTypeMappingApplier( + IModel model, + IRelationalTypeMappingSource typeMappingSource, + ISqlExpressionFactory sqlExpressionFactory, + IReadOnlyDictionary<(TableExpressionBase, string), RelationalTypeMapping> inferredTypeMappings) + : base(model, sqlExpressionFactory, inferredTypeMappings) + { + (_typeMappingSource, _sqlExpressionFactory) = (typeMappingSource, sqlExpressionFactory); + } + + /// + /// 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. + /// + protected override Expression VisitExtension(Expression expression) + { + switch (expression) + { + case SingleStoreJsonTableExpression { Name: "JSON_TABLE", Schema: null, IsBuiltIn: true } jsonTableExpression + when TryGetInferredTypeMapping(jsonTableExpression, "value", out var typeMapping): + return ApplyTypeMappingsOnJsonTableExpression(jsonTableExpression, typeMapping); + + // Above, we applied the type mapping the the parameter that JSON_TABLE accepts as an argument. + // But the inferred type mapping also needs to be applied as a SQL conversion on the column projections coming out of the + // SelectExpression containing the JSON_TABLE call. So we set state to know about JSON_TABLE tables and their type mappings + // in the immediate SelectExpression, and continue visiting down (see ColumnExpression visitation below). + case SelectExpression selectExpression: + { + Dictionary previousSelectInferredTypeMappings = null; + + foreach (var table in selectExpression.Tables) + { + if (table is TableValuedFunctionExpression { Name: "JSON_TABLE", Schema: null, IsBuiltIn: true } jsonTableExpression + && TryGetInferredTypeMapping(jsonTableExpression, "value", out var inferredTypeMapping)) + { + if (previousSelectInferredTypeMappings is null) + { + previousSelectInferredTypeMappings = _currentSelectInferredTypeMappings; + _currentSelectInferredTypeMappings = new Dictionary(); + } + + _currentSelectInferredTypeMappings![jsonTableExpression] = inferredTypeMapping; + } + } + + var visited = base.VisitExtension(expression); + + _currentSelectInferredTypeMappings = previousSelectInferredTypeMappings; + + return visited; + } + + // Note that we match also ColumnExpressions which already have a type mapping, i.e. coming out of column collections (as + // opposed to parameter collections, where the type mapping needs to be inferred). This is in order to apply SQL conversion + // logic later in the process, see note in TranslateCollection. + case ColumnExpression { Name: "value" } columnExpression + when _currentSelectInferredTypeMappings?.TryGetValue(columnExpression.Table, out var inferredTypeMapping) is true: + return ApplyJsonSqlConversion( + columnExpression.ApplyTypeMapping(inferredTypeMapping), + _sqlExpressionFactory, + inferredTypeMapping, + columnExpression.IsNullable); + + default: + return base.VisitExtension(expression); + } + } + + /// + /// 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. + /// + protected virtual TableValuedFunctionExpression ApplyTypeMappingsOnJsonTableExpression( + SingleStoreJsonTableExpression jsonTableExpression, + RelationalTypeMapping inferredTypeMapping) + { + // Constant queryables are translated to VALUES, no need for JSON. + // Column queryables have their type mapping from the model, so we don't ever need to apply an inferred mapping on them. + if (jsonTableExpression.JsonExpression is not SqlParameterExpression parameterExpression) + { + return jsonTableExpression; + } + + if (_typeMappingSource.FindMapping(parameterExpression.Type, Model, inferredTypeMapping) is not SingleStoreStringTypeMapping + parameterTypeMapping) + { + throw new InvalidOperationException("Type mapping for 'string' could not be found or was not a SingleStoreStringTypeMapping"); + } + + Check.DebugAssert(parameterTypeMapping.ElementTypeMapping != null, "Collection type mapping missing element mapping."); + + return jsonTableExpression.Update( + parameterExpression.ApplyTypeMapping(parameterTypeMapping), + jsonTableExpression.Path, + new[] + { + new SingleStoreJsonTableExpression.ColumnInfo + { + Name = "value", + TypeMapping = (RelationalTypeMapping)parameterTypeMapping.ElementTypeMapping, + Path = new[] { new PathSegment(_sqlExpressionFactory.Constant(0, _typeMappingSource.FindMapping(typeof(int)))) }, + } + }); + } + } + + private sealed class FakeMemberInfo : MemberInfo + { + public FakeMemberInfo(string name) + => Name = name; + + public override string Name { get; } + + public override object[] GetCustomAttributes(bool inherit) + => throw new NotSupportedException(); + public override object[] GetCustomAttributes(Type attributeType, bool inherit) + => throw new NotSupportedException(); + public override bool IsDefined(Type attributeType, bool inherit) + => throw new NotSupportedException(); + public override Type DeclaringType => throw new NotSupportedException(); + public override MemberTypes MemberType => throw new NotSupportedException(); + public override Type ReflectedType => throw new NotSupportedException(); + } } diff --git a/src/EFCore.SingleStore/Query/Internal/SingleStoreQueryableMethodTranslatingExpressionVisitorFactory.cs b/src/EFCore.SingleStore/Query/Internal/SingleStoreQueryableMethodTranslatingExpressionVisitorFactory.cs index 2cbf980fc..2fdab9391 100644 --- a/src/EFCore.SingleStore/Query/Internal/SingleStoreQueryableMethodTranslatingExpressionVisitorFactory.cs +++ b/src/EFCore.SingleStore/Query/Internal/SingleStoreQueryableMethodTranslatingExpressionVisitorFactory.cs @@ -3,11 +3,14 @@ // Licensed under the MIT. See LICENSE in the project root for license information. using Microsoft.EntityFrameworkCore.Query; +using EntityFrameworkCore.SingleStore.Infrastructure.Internal; namespace EntityFrameworkCore.SingleStore.Query.Internal; public class SingleStoreQueryableMethodTranslatingExpressionVisitorFactory : IQueryableMethodTranslatingExpressionVisitorFactory { + private readonly ISingleStoreOptions _options; + /// /// 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 @@ -16,10 +19,12 @@ public class SingleStoreQueryableMethodTranslatingExpressionVisitorFactory : IQu /// public SingleStoreQueryableMethodTranslatingExpressionVisitorFactory( QueryableMethodTranslatingExpressionVisitorDependencies dependencies, - RelationalQueryableMethodTranslatingExpressionVisitorDependencies relationalDependencies) + RelationalQueryableMethodTranslatingExpressionVisitorDependencies relationalDependencies, + ISingleStoreOptions options) { Dependencies = dependencies; RelationalDependencies = relationalDependencies; + _options = options; } /// @@ -39,5 +44,5 @@ public SingleStoreQueryableMethodTranslatingExpressionVisitorFactory( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual QueryableMethodTranslatingExpressionVisitor Create(QueryCompilationContext queryCompilationContext) - => new SingleStoreQueryableMethodTranslatingExpressionVisitor(Dependencies, RelationalDependencies, queryCompilationContext); + => new SingleStoreQueryableMethodTranslatingExpressionVisitor(Dependencies, RelationalDependencies, queryCompilationContext, _options); } diff --git a/src/EFCore.SingleStore/Query/Internal/SingleStoreSqlExpressionFactory.cs b/src/EFCore.SingleStore/Query/Internal/SingleStoreSqlExpressionFactory.cs index b19d9138d..c66f8c6ad 100644 --- a/src/EFCore.SingleStore/Query/Internal/SingleStoreSqlExpressionFactory.cs +++ b/src/EFCore.SingleStore/Query/Internal/SingleStoreSqlExpressionFactory.cs @@ -21,12 +21,14 @@ public class SingleStoreSqlExpressionFactory : SqlExpressionFactory { private readonly IRelationalTypeMappingSource _typeMappingSource; private readonly RelationalTypeMapping _boolTypeMapping; + private readonly RelationalTypeMapping _doubleTypeMapping; public SingleStoreSqlExpressionFactory(SqlExpressionFactoryDependencies dependencies) : base(dependencies) { _typeMappingSource = dependencies.TypeMappingSource; _boolTypeMapping = _typeMappingSource.FindMapping(typeof(bool)); + _doubleTypeMapping = _typeMappingSource.FindMapping(typeof(double)); } public virtual RelationalTypeMapping FindMapping( @@ -383,7 +385,7 @@ private SqlExpression ApplyTypeMappingOnMatch(SingleStoreMatchExpression matchEx return new SingleStoreMatchExpression( ApplyTypeMapping(matchExpression.Match, inferredTypeMapping), ApplyTypeMapping(matchExpression.Against, inferredTypeMapping), - _boolTypeMapping); + _doubleTypeMapping); } private SqlExpression ApplyTypeMappingOnRegexp(SingleStoreRegexpExpression regexpExpression) diff --git a/src/EFCore.SingleStore/Query/Internal/SingleStoreStringMethodTranslator.cs b/src/EFCore.SingleStore/Query/Internal/SingleStoreStringMethodTranslator.cs index 45109f428..92bcbc606 100644 --- a/src/EFCore.SingleStore/Query/Internal/SingleStoreStringMethodTranslator.cs +++ b/src/EFCore.SingleStore/Query/Internal/SingleStoreStringMethodTranslator.cs @@ -6,8 +6,6 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using Microsoft.EntityFrameworkCore.Storage; @@ -18,9 +16,10 @@ namespace EntityFrameworkCore.SingleStore.Query.Internal { - public class SingleStoreStringMethodTranslator : IMethodCallTranslator + public class SingleStoreStringMethodTranslator : SingleStoreQueryCompilationContextMethodTranslator { private readonly IRelationalTypeMappingSource _typeMappingSource; + private readonly Func _queryCompilationContextResolver; private readonly ISingleStoreOptions _options; private static readonly MethodInfo _indexOfMethodInfo @@ -117,28 +116,31 @@ private static readonly MethodInfo _removeMethodInfoWithTwoArgs public SingleStoreStringMethodTranslator( SingleStoreSqlExpressionFactory sqlExpressionFactory, SingleStoreTypeMappingSource typeMappingSource, + Func queryCompilationContextResolver, ISingleStoreOptions options) + : base(queryCompilationContextResolver) { _sqlExpressionFactory = sqlExpressionFactory; _typeMappingSource = typeMappingSource; + _queryCompilationContextResolver = queryCompilationContextResolver; _options = options; } - public virtual SqlExpression Translate( + public override SqlExpression Translate( SqlExpression instance, MethodInfo method, IReadOnlyList arguments, - IDiagnosticsLogger logger) + QueryCompilationContext queryCompilationContext) { if (_indexOfMethodInfo.Equals(method)) { - return new SingleStoreStringComparisonMethodTranslator(_sqlExpressionFactory, _options) + return new SingleStoreStringComparisonMethodTranslator(_sqlExpressionFactory, _queryCompilationContextResolver, _options) .MakeIndexOfExpression(instance, arguments[0]); } if(_indexOfMethodInfoWithOneArg.Equals(method)) { - return new SingleStoreStringComparisonMethodTranslator(_sqlExpressionFactory, _options) + return new SingleStoreStringComparisonMethodTranslator(_sqlExpressionFactory, _queryCompilationContextResolver, _options) .MakeIndexOfExpression(instance, arguments[0], startIndex: arguments[1]); } @@ -250,20 +252,20 @@ public virtual SqlExpression Translate( if (_containsMethodInfo.Equals(method)) { - return new SingleStoreStringComparisonMethodTranslator(_sqlExpressionFactory, _options) - .MakeContainsExpression(instance, arguments[0]); + return new SingleStoreStringComparisonMethodTranslator(_sqlExpressionFactory, _queryCompilationContextResolver, _options) + .MakeContainsExpression(queryCompilationContext, instance, arguments[0]); } if (_startsWithMethodInfo.Equals(method)) { - return new SingleStoreStringComparisonMethodTranslator(_sqlExpressionFactory, _options) - .MakeStartsWithExpression(instance, arguments[0]); + return new SingleStoreStringComparisonMethodTranslator(_sqlExpressionFactory, _queryCompilationContextResolver, _options) + .MakeStartsWithExpression(queryCompilationContext, instance, arguments[0]); } if (_endsWithMethodInfo.Equals(method)) { - return new SingleStoreStringComparisonMethodTranslator(_sqlExpressionFactory, _options) - .MakeEndsWithExpression(instance, arguments[0]); + return new SingleStoreStringComparisonMethodTranslator(_sqlExpressionFactory, _queryCompilationContextResolver, _options) + .MakeEndsWithExpression(queryCompilationContext, instance, arguments[0]); } if (_padLeftWithOneArg.Equals(method)) diff --git a/src/EFCore.SingleStore/Scaffolding/Internal/SingleStoreCodeGenerationMemberAccessTypeMapping.cs b/src/EFCore.SingleStore/Scaffolding/Internal/SingleStoreCodeGenerationMemberAccessTypeMapping.cs index 54f892757..4e7a2879b 100644 --- a/src/EFCore.SingleStore/Scaffolding/Internal/SingleStoreCodeGenerationMemberAccessTypeMapping.cs +++ b/src/EFCore.SingleStore/Scaffolding/Internal/SingleStoreCodeGenerationMemberAccessTypeMapping.cs @@ -12,6 +12,8 @@ internal class SingleStoreCodeGenerationMemberAccessTypeMapping : RelationalType { private const string DummyStoreType = "clrOnly"; + public static SingleStoreCodeGenerationMemberAccessTypeMapping Default { get; } = new(); + public SingleStoreCodeGenerationMemberAccessTypeMapping() : base(new RelationalTypeMappingParameters(new CoreTypeMappingParameters(typeof(SingleStoreCodeGenerationMemberAccess)), DummyStoreType)) { diff --git a/src/EFCore.SingleStore/Scaffolding/Internal/SingleStoreCodeGenerationServerVersionCreationTypeMapping.cs b/src/EFCore.SingleStore/Scaffolding/Internal/SingleStoreCodeGenerationServerVersionCreationTypeMapping.cs index c758da93b..8931f6b76 100644 --- a/src/EFCore.SingleStore/Scaffolding/Internal/SingleStoreCodeGenerationServerVersionCreationTypeMapping.cs +++ b/src/EFCore.SingleStore/Scaffolding/Internal/SingleStoreCodeGenerationServerVersionCreationTypeMapping.cs @@ -14,6 +14,8 @@ internal class SingleStoreCodeGenerationServerVersionCreationTypeMapping : Relat { private const string DummyStoreType = "clrOnly"; + public static SingleStoreCodeGenerationServerVersionCreationTypeMapping Default { get; } = new(); + public SingleStoreCodeGenerationServerVersionCreationTypeMapping() : base(new RelationalTypeMappingParameters(new CoreTypeMappingParameters(typeof(SingleStoreCodeGenerationServerVersionCreation)), DummyStoreType)) { diff --git a/src/EFCore.SingleStore/Scaffolding/Internal/SingleStoreDatabaseModelFactory.cs b/src/EFCore.SingleStore/Scaffolding/Internal/SingleStoreDatabaseModelFactory.cs index 00612373f..3ca095199 100644 --- a/src/EFCore.SingleStore/Scaffolding/Internal/SingleStoreDatabaseModelFactory.cs +++ b/src/EFCore.SingleStore/Scaffolding/Internal/SingleStoreDatabaseModelFactory.cs @@ -538,7 +538,7 @@ private bool IsDefaultValueSqlFunction(string defaultValue, string dataType) /// /// MariaDB 10.2.7+ implements default values differently from MySQL, to support their own default expression /// syntax. We convert their column values to MySQL compatible syntax here. - /// See https://github.com/PomeloFoundation/EntityFrameworkCore.SingleStore/issues/994#issuecomment-568271740 + /// See https://github.com/PomeloFoundation/EntityFrameworkCore.MySql/issues/994#issuecomment-568271740 /// for tables with differences. /// protected virtual string ConvertDefaultValueFromMariaDbToSingleStore([NotNull] string defaultValue, out bool isDefaultValueExpression) diff --git a/src/EFCore.SingleStore/Storage/Internal/ISingleStoreCSharpRuntimeAnnotationTypeMappingCodeGenerator.cs b/src/EFCore.SingleStore/Storage/Internal/ISingleStoreCSharpRuntimeAnnotationTypeMappingCodeGenerator.cs new file mode 100644 index 000000000..18d9a0343 --- /dev/null +++ b/src/EFCore.SingleStore/Storage/Internal/ISingleStoreCSharpRuntimeAnnotationTypeMappingCodeGenerator.cs @@ -0,0 +1,14 @@ +// Copyright (c) Pomelo Foundation. All rights reserved. +// Copyright (c) SingleStore Inc. All rights reserved. +// Licensed under the MIT. See LICENSE in the project root for license information. + +using Microsoft.EntityFrameworkCore.Design.Internal; + +namespace EntityFrameworkCore.SingleStore.Storage.Internal; + +public interface ISingleStoreCSharpRuntimeAnnotationTypeMappingCodeGenerator +{ + void Create( + CSharpRuntimeAnnotationCodeGeneratorParameters codeGeneratorParameters, + CSharpRuntimeAnnotationCodeGeneratorDependencies codeGeneratorDependencies); +} diff --git a/src/EFCore.SingleStore/Storage/Internal/ISingleStoreConnectionStringOptionsValidator.cs b/src/EFCore.SingleStore/Storage/Internal/ISingleStoreConnectionStringOptionsValidator.cs new file mode 100644 index 000000000..2832e0add --- /dev/null +++ b/src/EFCore.SingleStore/Storage/Internal/ISingleStoreConnectionStringOptionsValidator.cs @@ -0,0 +1,17 @@ +// Copyright (c) Pomelo Foundation. All rights reserved. +// Copyright (c) SingleStore Inc. All rights reserved. +// Licensed under the MIT. See LICENSE in the project root for license information. + +using System; +using System.Data.Common; + +namespace EntityFrameworkCore.SingleStore.Storage.Internal; + +public interface ISingleStoreConnectionStringOptionsValidator +{ + bool EnsureMandatoryOptions(ref string connectionString); + bool EnsureMandatoryOptions(DbConnection connection); + bool EnsureMandatoryOptions(DbDataSource dataSource); + + void ThrowException(Exception innerException = null); +} diff --git a/src/EFCore.SingleStore/Storage/Internal/Json/SingleStoreJsonByteArrayAsHexStringReaderWriter.cs b/src/EFCore.SingleStore/Storage/Internal/Json/SingleStoreJsonByteArrayAsHexStringReaderWriter.cs new file mode 100644 index 000000000..f8bc50c02 --- /dev/null +++ b/src/EFCore.SingleStore/Storage/Internal/Json/SingleStoreJsonByteArrayAsHexStringReaderWriter.cs @@ -0,0 +1,24 @@ +// Copyright (c) Pomelo Foundation. All rights reserved. +// Copyright (c) SingleStore Inc. All rights reserved. +// Licensed under the MIT. See LICENSE in the project root for license information. + +using System; +using System.Text.Json; +using Microsoft.EntityFrameworkCore.Storage.Json; + +namespace EntityFrameworkCore.SingleStore.Storage.Internal.Json; + +public sealed class SingleStoreJsonByteArrayAsHexStringReaderWriter : JsonValueReaderWriter +{ + public static SingleStoreJsonByteArrayAsHexStringReaderWriter Instance { get; } = new(); + + private SingleStoreJsonByteArrayAsHexStringReaderWriter() + { + } + + public override byte[] FromJsonTyped(ref Utf8JsonReaderManager manager, object existingObject = null) + => Convert.FromHexString(manager.CurrentReader.GetString()!); + + public override void ToJsonTyped(Utf8JsonWriter writer, byte[] value) + => writer.WriteStringValue(Convert.ToHexString(value)); +} diff --git a/src/EFCore.SingleStore/Storage/Internal/SingleStoreBoolTypeMapping.cs b/src/EFCore.SingleStore/Storage/Internal/SingleStoreBoolTypeMapping.cs index 98397df00..f61273b1f 100644 --- a/src/EFCore.SingleStore/Storage/Internal/SingleStoreBoolTypeMapping.cs +++ b/src/EFCore.SingleStore/Storage/Internal/SingleStoreBoolTypeMapping.cs @@ -5,6 +5,7 @@ using System.Data; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace EntityFrameworkCore.SingleStore.Storage.Internal { @@ -16,16 +17,21 @@ namespace EntityFrameworkCore.SingleStore.Storage.Internal /// public class SingleStoreBoolTypeMapping : BoolTypeMapping { + public static new SingleStoreBoolTypeMapping Default { get; } = new("tinyint", size: 1); + public SingleStoreBoolTypeMapping( [NotNull] string storeType, - DbType? dbType = null, + DbType? dbType = System.Data.DbType.Boolean, int? size = null) - : this(new RelationalTypeMappingParameters( - new CoreTypeMappingParameters(typeof(bool)), - storeType, - size == null ? StoreTypePostfix.None : StoreTypePostfix.Size, - dbType, - size: size)) + : this( + new RelationalTypeMappingParameters( + new CoreTypeMappingParameters( + typeof(bool), + jsonValueReaderWriter: JsonBoolReaderWriter.Instance), + storeType, + size == null ? StoreTypePostfix.None : StoreTypePostfix.Size, + dbType, + size: size)) { } diff --git a/src/EFCore.SingleStore/Storage/Internal/SingleStoreByteArrayTypeMapping.cs b/src/EFCore.SingleStore/Storage/Internal/SingleStoreByteArrayTypeMapping.cs index 9c350744c..273fca305 100644 --- a/src/EFCore.SingleStore/Storage/Internal/SingleStoreByteArrayTypeMapping.cs +++ b/src/EFCore.SingleStore/Storage/Internal/SingleStoreByteArrayTypeMapping.cs @@ -6,7 +6,8 @@ using System.Data; using System.Data.Common; using Microsoft.EntityFrameworkCore.Storage; -using EntityFrameworkCore.SingleStore.Utilities; +using Microsoft.EntityFrameworkCore.Storage.Json; +using EntityFrameworkCore.SingleStore.Storage.Internal.Json; namespace EntityFrameworkCore.SingleStore.Storage.Internal { @@ -20,6 +21,8 @@ public class SingleStoreByteArrayTypeMapping : ByteArrayTypeMapping private readonly int _maxSpecificSize; + public static new SingleStoreByteArrayTypeMapping Default { get; } = new(); + /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. @@ -46,7 +49,9 @@ protected SingleStoreByteArrayTypeMapping( bool fixedLength) : this( new RelationalTypeMappingParameters( - new CoreTypeMappingParameters(typeof(byte[])), + new CoreTypeMappingParameters( + typeof(byte[]), + jsonValueReaderWriter: JsonByteArrayReaderWriter.Instance /* SingleStoreJsonByteArrayAsHexStringReaderWriter.Instance */), storeType ?? GetBaseType(size, fixedLength), GetStoreTypePostfix(size), type, @@ -110,6 +115,9 @@ protected override void ConfigureParameter(DbParameter parameter) /// /// The generated string. /// - protected override string GenerateNonNullSqlLiteral(object value) => ByteArrayFormatter.ToHex((byte[])value); + protected override string GenerateNonNullSqlLiteral(object value) + => value is byte[] { Length: > 0 } byteArray + ? "0x" + Convert.ToHexString(byteArray) + : "X''"; } } diff --git a/src/EFCore.SingleStore/Storage/Internal/SingleStoreByteTypeMapping.cs b/src/EFCore.SingleStore/Storage/Internal/SingleStoreByteTypeMapping.cs new file mode 100644 index 000000000..0c82eab1b --- /dev/null +++ b/src/EFCore.SingleStore/Storage/Internal/SingleStoreByteTypeMapping.cs @@ -0,0 +1,28 @@ +// Copyright (c) Pomelo Foundation. All rights reserved. +// Copyright (c) SingleStore Inc. All rights reserved. +// Licensed under the MIT. See LICENSE in the project root for license information. + +using System.Data; +using Microsoft.EntityFrameworkCore.Storage; + +namespace EntityFrameworkCore.SingleStore.Storage.Internal; + +public class SingleStoreByteTypeMapping : ByteTypeMapping +{ + public static new SingleStoreByteTypeMapping Default { get; } = new("tinyint unsigned"); + + public SingleStoreByteTypeMapping( + string storeType, + DbType? dbType = System.Data.DbType.Byte) + : base(storeType, dbType) + { + } + + protected SingleStoreByteTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new SingleStoreByteTypeMapping(parameters); +} diff --git a/src/EFCore.SingleStore/Storage/Internal/SingleStoreConnectionStringOptionsValidator.cs b/src/EFCore.SingleStore/Storage/Internal/SingleStoreConnectionStringOptionsValidator.cs new file mode 100644 index 000000000..373d5f1c9 --- /dev/null +++ b/src/EFCore.SingleStore/Storage/Internal/SingleStoreConnectionStringOptionsValidator.cs @@ -0,0 +1,113 @@ +// Copyright (c) Pomelo Foundation. All rights reserved. +// Copyright (c) SingleStore Inc. All rights reserved. +// Licensed under the MIT. See LICENSE in the project root for license information. + +using System; +using System.Data.Common; +using System.Linq; +using System.Reflection; +using SingleStoreConnector; + +namespace EntityFrameworkCore.SingleStore.Storage.Internal; + +public class SingleStoreConnectionStringOptionsValidator : ISingleStoreConnectionStringOptionsValidator +{ + public virtual bool EnsureMandatoryOptions(ref string connectionString) + { + if (connectionString is not null) + { + var csb = new SingleStoreConnectionStringBuilder(connectionString); + AddConnectionAttributes(csb); + + if (!ValidateMandatoryOptions(csb)) + { + csb.AllowUserVariables = true; + csb.UseAffectedRows = false; + + connectionString = csb.ConnectionString; + + return true; + } + } + + return false; + } + + public virtual bool EnsureMandatoryOptions(DbConnection connection) + { + if (connection is not null) + { + var csb = new SingleStoreConnectionStringBuilder(connection.ConnectionString); + AddConnectionAttributes(csb); + + if (!ValidateMandatoryOptions(csb)) + { + try + { + csb.AllowUserVariables = true; + csb.UseAffectedRows = false; + + connection.ConnectionString = csb.ConnectionString; + + return true; + } + catch (Exception e) + { + ThrowException(e); + } + } + } + + return false; + } + + public virtual bool EnsureMandatoryOptions(DbDataSource dataSource) + { + if (dataSource is null) + { + return false; + } + + var csb = new SingleStoreConnectionStringBuilder(dataSource.ConnectionString); + AddConnectionAttributes(csb); + + if (!ValidateMandatoryOptions(csb)) + { + // We can't alter the connection string of a DbDataSource/SingleStoreDataSource as we do for DbConnection/SingleStoreConnection in cases + // where the necessary connection string options have not been set. + // We can only throw. + ThrowException(); + } + + return true; + } + + static void AddConnectionAttributes(SingleStoreConnectionStringBuilder csb) + { + var existing = csb.ConnectionAttributes?.TrimEnd(',') ?? ""; + + var programVersion = Assembly.GetExecutingAssembly().GetName().Version; + var connAttrs = $"_connector_name:SingleStore Entity Framework Core provider,_connector_version:{programVersion}"; + + var existingConnAttrs = existing + .Split(',', StringSplitOptions.RemoveEmptyEntries) + .Select(attr => attr.Trim()) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + if (!existingConnAttrs.Contains(connAttrs)) + { + csb.ConnectionAttributes = string.IsNullOrEmpty(existing) + ? connAttrs + : $"{existing},{connAttrs}"; + } + } + + public virtual void ThrowException(Exception innerException = null) + => throw new InvalidOperationException( + @"The connection string of a connection used by EntityFrameworkCore.SingleStore must contain ""AllowUserVariables=True;UseAffectedRows=False"".", + innerException); + + protected virtual bool ValidateMandatoryOptions(SingleStoreConnectionStringBuilder csb) + => csb.AllowUserVariables && + !csb.UseAffectedRows; +} diff --git a/src/EFCore.SingleStore/Storage/Internal/SingleStoreDateTimeOffsetTypeMapping.cs b/src/EFCore.SingleStore/Storage/Internal/SingleStoreDateTimeOffsetTypeMapping.cs index eb43639cd..622d53cde 100644 --- a/src/EFCore.SingleStore/Storage/Internal/SingleStoreDateTimeOffsetTypeMapping.cs +++ b/src/EFCore.SingleStore/Storage/Internal/SingleStoreDateTimeOffsetTypeMapping.cs @@ -6,6 +6,7 @@ using System.Data.Common; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace EntityFrameworkCore.SingleStore.Storage.Internal { @@ -19,6 +20,8 @@ public class SingleStoreDateTimeOffsetTypeMapping : DateTimeOffsetTypeMapping, I { private readonly bool _isDefaultValueCompatible; + public static new SingleStoreDateTimeOffsetTypeMapping Default { get; } = new("datetime"); + /// /// 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 @@ -31,7 +34,9 @@ public SingleStoreDateTimeOffsetTypeMapping( bool isDefaultValueCompatible = true) : this( new RelationalTypeMappingParameters( - new CoreTypeMappingParameters(typeof(DateTimeOffset)), + new CoreTypeMappingParameters( + typeof(DateTimeOffset), + jsonValueReaderWriter: JsonDateTimeOffsetReaderWriter.Instance), storeType, StoreTypePostfix.Precision, System.Data.DbType.DateTimeOffset, diff --git a/src/EFCore.SingleStore/Storage/Internal/SingleStoreDateTimeTypeMapping.cs b/src/EFCore.SingleStore/Storage/Internal/SingleStoreDateTimeTypeMapping.cs index b742d8495..8b79d02fb 100644 --- a/src/EFCore.SingleStore/Storage/Internal/SingleStoreDateTimeTypeMapping.cs +++ b/src/EFCore.SingleStore/Storage/Internal/SingleStoreDateTimeTypeMapping.cs @@ -6,6 +6,7 @@ using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Json; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace EntityFrameworkCore.SingleStore.Storage.Internal @@ -20,6 +21,8 @@ public class SingleStoreDateTimeTypeMapping : DateTimeTypeMapping, IDefaultValue { private readonly bool _isDefaultValueCompatible; + public static new SingleStoreDateTimeTypeMapping Default { get; } = new("datetime"); + /// /// 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 @@ -35,7 +38,11 @@ public SingleStoreDateTimeTypeMapping( bool isDefaultValueCompatible = true) : this( new RelationalTypeMappingParameters( - new CoreTypeMappingParameters(clrType ?? typeof(DateTime), converter, comparer), + new CoreTypeMappingParameters( + clrType ?? typeof(DateTime), + converter, + comparer, + jsonValueReaderWriter: JsonDateTimeReaderWriter.Instance), storeType, StoreTypePostfix.Precision, System.Data.DbType.DateTime, diff --git a/src/EFCore.SingleStore/Storage/Internal/SingleStoreDateTypeMapping.cs b/src/EFCore.SingleStore/Storage/Internal/SingleStoreDateTypeMapping.cs index 5c56a09c0..41d981a6d 100644 --- a/src/EFCore.SingleStore/Storage/Internal/SingleStoreDateTypeMapping.cs +++ b/src/EFCore.SingleStore/Storage/Internal/SingleStoreDateTypeMapping.cs @@ -5,6 +5,7 @@ using System; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace EntityFrameworkCore.SingleStore.Storage.Internal { @@ -21,6 +22,8 @@ public class SingleStoreDateTypeMapping : RelationalTypeMapping, IDefaultValueCo { private readonly bool _isDefaultValueCompatible; + public static SingleStoreDateTypeMapping Default { get; } = new("date", typeof(DateOnly)); + /// /// 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,9 @@ public class SingleStoreDateTypeMapping : RelationalTypeMapping, IDefaultValueCo public SingleStoreDateTypeMapping([NotNull] string storeType, Type clrType, bool isDefaultValueCompatible = true) : this( new RelationalTypeMappingParameters( - new CoreTypeMappingParameters(clrType), + new CoreTypeMappingParameters( + clrType, + jsonValueReaderWriter: JsonDateOnlyReaderWriter.Instance), storeType, dbType: System.Data.DbType.Date), isDefaultValueCompatible) diff --git a/src/EFCore.SingleStore/Storage/Internal/SingleStoreDecimalTypeMapping.cs b/src/EFCore.SingleStore/Storage/Internal/SingleStoreDecimalTypeMapping.cs index e272ad5ca..6faa63e92 100644 --- a/src/EFCore.SingleStore/Storage/Internal/SingleStoreDecimalTypeMapping.cs +++ b/src/EFCore.SingleStore/Storage/Internal/SingleStoreDecimalTypeMapping.cs @@ -6,24 +6,29 @@ using System.Data.Common; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Storage; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace EntityFrameworkCore.SingleStore.Storage.Internal { public class SingleStoreDecimalTypeMapping : DecimalTypeMapping { + public static new SingleStoreDecimalTypeMapping Default { get; } = new("decimal", precision: 65, scale: 30); + /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// - public SingleStoreDecimalTypeMapping([NotNull] string storeType, + public SingleStoreDecimalTypeMapping( + [NotNull] string storeType, DbType? dbType = null, int? precision = null, int? scale = null, StoreTypePostfix storeTypePostfix = StoreTypePostfix.PrecisionAndScale) - : this( + : base( new RelationalTypeMappingParameters( - new CoreTypeMappingParameters(typeof(decimal)), + new CoreTypeMappingParameters( + typeof(decimal), + jsonValueReaderWriter: JsonDecimalReaderWriter.Instance), storeType, storeTypePostfix, dbType) diff --git a/src/EFCore.SingleStore/Storage/Internal/SingleStoreDoubleTypeMapping.cs b/src/EFCore.SingleStore/Storage/Internal/SingleStoreDoubleTypeMapping.cs index 97ca71a8d..fa14b22f2 100644 --- a/src/EFCore.SingleStore/Storage/Internal/SingleStoreDoubleTypeMapping.cs +++ b/src/EFCore.SingleStore/Storage/Internal/SingleStoreDoubleTypeMapping.cs @@ -6,7 +6,6 @@ using System.Data.Common; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Storage; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace EntityFrameworkCore.SingleStore.Storage.Internal { @@ -18,6 +17,8 @@ namespace EntityFrameworkCore.SingleStore.Storage.Internal /// public class SingleStoreDoubleTypeMapping : DoubleTypeMapping { + public static new SingleStoreDoubleTypeMapping Default { get; } = new("double"); + /// /// 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 @@ -26,7 +27,7 @@ public class SingleStoreDoubleTypeMapping : DoubleTypeMapping /// public SingleStoreDoubleTypeMapping( [NotNull] string storeType, - DbType? dbType = null) + DbType? dbType = System.Data.DbType.Double) : base(storeType, dbType) { } diff --git a/src/EFCore.SingleStore/Storage/Internal/SingleStoreFloatTypeMapping.cs b/src/EFCore.SingleStore/Storage/Internal/SingleStoreFloatTypeMapping.cs index 4138cbdd8..454af004d 100644 --- a/src/EFCore.SingleStore/Storage/Internal/SingleStoreFloatTypeMapping.cs +++ b/src/EFCore.SingleStore/Storage/Internal/SingleStoreFloatTypeMapping.cs @@ -7,12 +7,13 @@ using System.Globalization; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Storage; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace EntityFrameworkCore.SingleStore.Storage.Internal { public class SingleStoreFloatTypeMapping : FloatTypeMapping { + public static new SingleStoreFloatTypeMapping Default { get; } = new("float"); + /// /// 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 @@ -21,7 +22,7 @@ public class SingleStoreFloatTypeMapping : FloatTypeMapping /// public SingleStoreFloatTypeMapping( [NotNull] string storeType, - DbType? dbType = null) + DbType? dbType = System.Data.DbType.Single) : base(storeType, dbType) { } diff --git a/src/EFCore.SingleStore/Storage/Internal/SingleStoreGuidTypeMapping.cs b/src/EFCore.SingleStore/Storage/Internal/SingleStoreGuidTypeMapping.cs index 51e5e3a13..dd66be438 100644 --- a/src/EFCore.SingleStore/Storage/Internal/SingleStoreGuidTypeMapping.cs +++ b/src/EFCore.SingleStore/Storage/Internal/SingleStoreGuidTypeMapping.cs @@ -3,19 +3,26 @@ // Licensed under the MIT. See LICENSE in the project root for license information. using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore.Design.Internal; using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Json; using SingleStoreConnector; -using EntityFrameworkCore.SingleStore.Utilities; namespace EntityFrameworkCore.SingleStore.Storage.Internal { - public class SingleStoreGuidTypeMapping : GuidTypeMapping, IJsonSpecificTypeMapping + public class SingleStoreGuidTypeMapping : GuidTypeMapping, IJsonSpecificTypeMapping, ISingleStoreCSharpRuntimeAnnotationTypeMappingCodeGenerator { - private readonly SingleStoreGuidFormat _guidFormat; + public static new SingleStoreGuidTypeMapping Default { get; } = new(SingleStoreGuidFormat.Char36); + + public virtual SingleStoreGuidFormat GuidFormat { get; } public SingleStoreGuidTypeMapping(SingleStoreGuidFormat guidFormat) : this(new RelationalTypeMappingParameters( - new CoreTypeMappingParameters(typeof(Guid)), + new CoreTypeMappingParameters( + typeof(Guid), + jsonValueReaderWriter: JsonGuidReaderWriter.Instance), GetStoreType(guidFormat), StoreTypePostfix.Size, System.Data.DbType.Guid, @@ -29,18 +36,21 @@ public SingleStoreGuidTypeMapping(SingleStoreGuidFormat guidFormat) protected SingleStoreGuidTypeMapping(RelationalTypeMappingParameters parameters, SingleStoreGuidFormat guidFormat) : base(parameters) { - _guidFormat = guidFormat; + GuidFormat = guidFormat; } protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new SingleStoreGuidTypeMapping(parameters, _guidFormat); + => new SingleStoreGuidTypeMapping(parameters, GuidFormat); + + public virtual RelationalTypeMapping Clone(SingleStoreGuidFormat guidFormat) + => new SingleStoreGuidTypeMapping(Parameters, guidFormat); public virtual bool IsCharBasedStoreType - => GetStoreType(_guidFormat) == "char"; + => GetStoreType(GuidFormat) == "char"; protected override string GenerateNonNullSqlLiteral(object value) { - switch (_guidFormat) + switch (GuidFormat) { case SingleStoreGuidFormat.Char36: return $"'{value:D}'"; @@ -51,7 +61,7 @@ protected override string GenerateNonNullSqlLiteral(object value) case SingleStoreGuidFormat.Binary16: case SingleStoreGuidFormat.TimeSwapBinary16: case SingleStoreGuidFormat.LittleEndianBinary16: - return ByteArrayFormatter.ToHex(GetBytesFromGuid(_guidFormat, (Guid)value)); + return "0x" + Convert.ToHexString(GetBytesFromGuid(GuidFormat, (Guid)value)); case SingleStoreGuidFormat.None: case SingleStoreGuidFormat.Default: @@ -128,5 +138,50 @@ protected static byte[] GetBytesFromGuid(SingleStoreGuidFormat guidFormat, Guid /// public virtual RelationalTypeMapping CloneAsJsonCompatible() => new SingleStoreGuidTypeMapping(SingleStoreGuidFormat.Char36); + + void ISingleStoreCSharpRuntimeAnnotationTypeMappingCodeGenerator.Create( + CSharpRuntimeAnnotationCodeGeneratorParameters codeGeneratorParameters, + CSharpRuntimeAnnotationCodeGeneratorDependencies codeGeneratorDependencies) + { + var defaultTypeMapping = Default; + if (defaultTypeMapping == this) + { + return; + } + + var code = codeGeneratorDependencies.CSharpHelper; + + var cloneParameters = new List(); + + if (GuidFormat != defaultTypeMapping.GuidFormat) + { + cloneParameters.Add($"guidFormat: {code.Literal(GuidFormat, true)}"); + } + + if (cloneParameters.Any()) + { + var mainBuilder = codeGeneratorParameters.MainBuilder; + + mainBuilder.AppendLine(";"); + + mainBuilder + .AppendLine($"{codeGeneratorParameters.TargetName}.TypeMapping = (({code.Reference(GetType())}){codeGeneratorParameters.TargetName}.TypeMapping).Clone(") + .IncrementIndent(); + + for (var i = 0; i < cloneParameters.Count; i++) + { + if (i > 0) + { + mainBuilder.AppendLine(","); + } + + mainBuilder.Append(cloneParameters[i]); + } + + mainBuilder + .Append(")") + .DecrementIndent(); + } + } } } diff --git a/src/EFCore.SingleStore/Storage/Internal/SingleStoreIntTypeMapping.cs b/src/EFCore.SingleStore/Storage/Internal/SingleStoreIntTypeMapping.cs new file mode 100644 index 000000000..293392bdc --- /dev/null +++ b/src/EFCore.SingleStore/Storage/Internal/SingleStoreIntTypeMapping.cs @@ -0,0 +1,28 @@ +// Copyright (c) Pomelo Foundation. All rights reserved. +// Copyright (c) SingleStore Inc. All rights reserved. +// Licensed under the MIT. See LICENSE in the project root for license information. + +using System.Data; +using Microsoft.EntityFrameworkCore.Storage; + +namespace EntityFrameworkCore.SingleStore.Storage.Internal; + +public class SingleStoreIntTypeMapping : IntTypeMapping +{ + public static new SingleStoreIntTypeMapping Default { get; } = new("int"); + + public SingleStoreIntTypeMapping( + string storeType, + DbType? dbType = System.Data.DbType.Int32) + : base(storeType, dbType) + { + } + + protected SingleStoreIntTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new SingleStoreIntTypeMapping(parameters); +} diff --git a/src/EFCore.SingleStore/Storage/Internal/SingleStoreJsonTypeMapping.cs b/src/EFCore.SingleStore/Storage/Internal/SingleStoreJsonTypeMapping.cs index d38d7b0e4..914ddf1b6 100644 --- a/src/EFCore.SingleStore/Storage/Internal/SingleStoreJsonTypeMapping.cs +++ b/src/EFCore.SingleStore/Storage/Internal/SingleStoreJsonTypeMapping.cs @@ -3,56 +3,68 @@ // Licensed under the MIT. See LICENSE in the project root for license information. using System; +using System.Collections.Generic; using System.Data.Common; +using System.Linq; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Design.Internal; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using SingleStoreConnector; -using EntityFrameworkCore.SingleStore.Infrastructure.Internal; namespace EntityFrameworkCore.SingleStore.Storage.Internal { public class SingleStoreJsonTypeMapping : SingleStoreJsonTypeMapping { + public static new SingleStoreJsonTypeMapping Default { get; } = new("json", null, null, false, true); + public SingleStoreJsonTypeMapping( [NotNull] string storeType, [CanBeNull] ValueConverter valueConverter, [CanBeNull] ValueComparer valueComparer, - [NotNull] ISingleStoreOptions options) + bool noBackslashEscapes, + bool replaceLineBreaksWithCharFunction) : base( storeType, typeof(T), valueConverter, valueComparer, - options) + noBackslashEscapes, + replaceLineBreaksWithCharFunction) { } protected SingleStoreJsonTypeMapping( RelationalTypeMappingParameters parameters, SingleStoreDbType mySqlDbType, - ISingleStoreOptions options) - : base(parameters, mySqlDbType, options) + bool noBackslashEscapes, + bool replaceLineBreaksWithCharFunction) + : base(parameters, mySqlDbType, noBackslashEscapes, replaceLineBreaksWithCharFunction) { } protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new SingleStoreJsonTypeMapping(parameters, SingleStoreDbType, Options); + => new SingleStoreJsonTypeMapping(parameters, SingleStoreDbType, NoBackslashEscapes, ReplaceLineBreaksWithCharFunction); + + protected override RelationalTypeMapping Clone(bool? noBackslashEscapes = null, bool? replaceLineBreaksWithCharFunction = null) + => new SingleStoreJsonTypeMapping( + Parameters, + SingleStoreDbType, + noBackslashEscapes ?? NoBackslashEscapes, + replaceLineBreaksWithCharFunction ?? ReplaceLineBreaksWithCharFunction); } - public abstract class SingleStoreJsonTypeMapping : SingleStoreStringTypeMapping + public abstract class SingleStoreJsonTypeMapping : SingleStoreStringTypeMapping, ISingleStoreCSharpRuntimeAnnotationTypeMappingCodeGenerator { - [NotNull] - protected virtual ISingleStoreOptions Options { get; } - public SingleStoreJsonTypeMapping( [NotNull] string storeType, [NotNull] Type clrType, [CanBeNull] ValueConverter valueConverter, [CanBeNull] ValueComparer valueComparer, - [NotNull] ISingleStoreOptions options) + bool noBackslashEscapes, + bool replaceLineBreaksWithCharFunction) : base( new RelationalTypeMappingParameters( new CoreTypeMappingParameters( @@ -62,7 +74,8 @@ public SingleStoreJsonTypeMapping( storeType, unicode: true), SingleStoreDbType.JSON, - options, + noBackslashEscapes, + replaceLineBreaksWithCharFunction, false, false) { @@ -70,19 +83,30 @@ public SingleStoreJsonTypeMapping( { throw new ArgumentException($"The store type '{nameof(storeType)}' must be 'json'.", nameof(storeType)); } - - Options = options; } protected SingleStoreJsonTypeMapping( RelationalTypeMappingParameters parameters, SingleStoreDbType mySqlDbType, - ISingleStoreOptions options) - : base(parameters, mySqlDbType, options, false, false) + bool noBackslashEscapes, + bool replaceLineBreaksWithCharFunction) + : base( + parameters, + mySqlDbType, + noBackslashEscapes, + replaceLineBreaksWithCharFunction, + isUnquoted: false, + forceToString: false) { - Options = options; } + /// + /// Supports compiled models via ISingleStoreCSharpRuntimeAnnotationTypeMappingCodeGenerator.Create. + /// + protected abstract RelationalTypeMapping Clone( + bool? noBackslashEscapes = null, + bool? replaceLineBreaksWithCharFunction = null); + protected override void ConfigureParameter(DbParameter parameter) { base.ConfigureParameter(parameter); @@ -94,5 +118,55 @@ protected override void ConfigureParameter(DbParameter parameter) parameter.Value = (string)mySqlJsonString; } } + + void ISingleStoreCSharpRuntimeAnnotationTypeMappingCodeGenerator.Create( + CSharpRuntimeAnnotationCodeGeneratorParameters codeGeneratorParameters, + CSharpRuntimeAnnotationCodeGeneratorDependencies codeGeneratorDependencies) + { + var defaultTypeMapping = Default; + if (defaultTypeMapping == this) + { + return; + } + + var code = codeGeneratorDependencies.CSharpHelper; + + var cloneParameters = new List(); + + if (NoBackslashEscapes != defaultTypeMapping.NoBackslashEscapes) + { + cloneParameters.Add($"noBackslashEscapes: {code.Literal(NoBackslashEscapes)}"); + } + + if (ReplaceLineBreaksWithCharFunction != defaultTypeMapping.ReplaceLineBreaksWithCharFunction) + { + cloneParameters.Add($"replaceLineBreaksWithCharFunction: {code.Literal(ReplaceLineBreaksWithCharFunction)}"); + } + + if (cloneParameters.Any()) + { + var mainBuilder = codeGeneratorParameters.MainBuilder; + + mainBuilder.AppendLine(";"); + + mainBuilder + .AppendLine($"{codeGeneratorParameters.TargetName}.TypeMapping = (({code.Reference(GetType())}){codeGeneratorParameters.TargetName}.TypeMapping).Clone(") + .IncrementIndent(); + + for (var i = 0; i < cloneParameters.Count; i++) + { + if (i > 0) + { + mainBuilder.AppendLine(","); + } + + mainBuilder.Append(cloneParameters[i]); + } + + mainBuilder + .Append(")") + .DecrementIndent(); + } + } } } diff --git a/src/EFCore.SingleStore/Storage/Internal/SingleStoreJsonTypeMappingSourcePlugin.cs b/src/EFCore.SingleStore/Storage/Internal/SingleStoreJsonTypeMappingSourcePlugin.cs index 1a3f13dec..5d0e1487d 100644 --- a/src/EFCore.SingleStore/Storage/Internal/SingleStoreJsonTypeMappingSourcePlugin.cs +++ b/src/EFCore.SingleStore/Storage/Internal/SingleStoreJsonTypeMappingSourcePlugin.cs @@ -44,7 +44,8 @@ public virtual RelationalTypeMapping FindMapping(in RelationalTypeMappingInfo ma storeTypeName, GetValueConverter(clrType), GetValueComparer(clrType), - Options) + Options.NoBackslashEscapes, + Options.ReplaceLineBreaksWithCharFunction) : null; } diff --git a/src/EFCore.SingleStore/Storage/Internal/SingleStoreLongTypeMapping.cs b/src/EFCore.SingleStore/Storage/Internal/SingleStoreLongTypeMapping.cs new file mode 100644 index 000000000..ce0fecd82 --- /dev/null +++ b/src/EFCore.SingleStore/Storage/Internal/SingleStoreLongTypeMapping.cs @@ -0,0 +1,28 @@ +// Copyright (c) Pomelo Foundation. All rights reserved. +// Copyright (c) SingleStore Inc. All rights reserved. +// Licensed under the MIT. See LICENSE in the project root for license information. + +using System.Data; +using Microsoft.EntityFrameworkCore.Storage; + +namespace EntityFrameworkCore.SingleStore.Storage.Internal; + +public class SingleStoreLongTypeMapping : LongTypeMapping +{ + public static new SingleStoreLongTypeMapping Default { get; } = new("bigint"); + + public SingleStoreLongTypeMapping( + string storeType, + DbType? dbType = System.Data.DbType.Int64) + : base(storeType, dbType) + { + } + + protected SingleStoreLongTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new SingleStoreLongTypeMapping(parameters); +} diff --git a/src/EFCore.SingleStore/Storage/Internal/SingleStoreRelationalConnection.cs b/src/EFCore.SingleStore/Storage/Internal/SingleStoreRelationalConnection.cs index d4d692344..1c20ba5f0 100644 --- a/src/EFCore.SingleStore/Storage/Internal/SingleStoreRelationalConnection.cs +++ b/src/EFCore.SingleStore/Storage/Internal/SingleStoreRelationalConnection.cs @@ -12,6 +12,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.Extensions.DependencyInjection; using SingleStoreConnector; using EntityFrameworkCore.SingleStore.Infrastructure; using EntityFrameworkCore.SingleStore.Infrastructure.Internal; @@ -20,25 +21,140 @@ namespace EntityFrameworkCore.SingleStore.Storage.Internal { public class SingleStoreRelationalConnection : RelationalConnection, ISingleStoreRelationalConnection { + private readonly ISingleStoreConnectionStringOptionsValidator _mySqlConnectionStringOptionsValidator; private const string NoBackslashEscapes = "NO_BACKSLASH_ESCAPES"; private readonly SingleStoreOptionsExtension _mySqlOptionsExtension; + private DbDataSource _dataSource; + + public SingleStoreRelationalConnection( + RelationalConnectionDependencies dependencies, + ISingleStoreConnectionStringOptionsValidator mySqlConnectionStringOptionsValidator, + ISingleStoreOptions mySqlSingletonOptions) + : this( + dependencies, + mySqlConnectionStringOptionsValidator, + GetEffectiveDataSource(mySqlSingletonOptions, dependencies.ContextOptions)) + { + } - // ReSharper disable once VirtualMemberCallInConstructor - public SingleStoreRelationalConnection(RelationalConnectionDependencies dependencies) + public SingleStoreRelationalConnection( + RelationalConnectionDependencies dependencies, + ISingleStoreConnectionStringOptionsValidator mySqlConnectionStringOptionsValidator, + DbDataSource dataSource) : base(dependencies) { - _mySqlOptionsExtension = Dependencies.ContextOptions.FindExtension() ?? new SingleStoreOptionsExtension(); + _mySqlOptionsExtension = dependencies.ContextOptions.FindExtension() ?? + new SingleStoreOptionsExtension(); + _mySqlConnectionStringOptionsValidator = mySqlConnectionStringOptionsValidator; + + if (dataSource is not null) + { + _mySqlConnectionStringOptionsValidator.EnsureMandatoryOptions(dataSource); + + base.SetDbConnection(null, false); + base.ConnectionString = null; + + _dataSource = dataSource; + } + else if (base.ConnectionString is { } connectionString) + { + // This branch works for both: connections and connection strings, because base.ConnectionString handles both cases + // appropriately. + if (_mySqlConnectionStringOptionsValidator.EnsureMandatoryOptions(ref connectionString)) + { + try + { + base.ConnectionString = connectionString; + } + catch (Exception e) + { + _mySqlConnectionStringOptionsValidator.ThrowException(e); + } + } + } } + /// + /// We allow users to either explicitly set a DbDataSource using our `SingleStoreOptionsExtensions` or by adding it as a service via DI + /// (`ApplicationServiceProvider`). + /// We don't set a DI injected service to the `SingleStoreOption.DbDataSource` property, because it might get cached by the service + /// collection cache, since no relevant property might have changed in the `SingleStoreOptionsExtension` instance. If we would create + /// a similar DbContext instance with a different service collection later, EF Core would provide us with the *same* `SingleStoreOptions` + /// instance (that was cached before) and we would use the old `DbDataSource` instance that we retrieved from the old + /// `ApplicationServiceProvider`. + /// Therefore, we check the `ISingleStoreOptions.DbDataSource` property and the current `ApplicationServiceProvider` at the time we + /// actually need the instance. + /// + protected static DbDataSource GetEffectiveDataSource(ISingleStoreOptions mySqlSingletonOptions, IDbContextOptions contextOptions) + => mySqlSingletonOptions.DataSource ?? + contextOptions.FindExtension()?.ApplicationServiceProvider?.GetService(); + // TODO: Remove, because we don't use it anywhere. private bool IsMasterConnection { get; set; } protected override DbConnection CreateDbConnection() - => new SingleStoreConnection(AddConnectionStringOptions(new SingleStoreConnectionStringBuilder(ConnectionString!)).ConnectionString); + => _dataSource is not null + ? _dataSource.CreateConnection() + : new SingleStoreConnection(AddConnectionStringOptions(new SingleStoreConnectionStringBuilder(ConnectionString!)).ConnectionString); + + public override string ConnectionString + { + get => _dataSource is null + ? base.ConnectionString + : _dataSource.ConnectionString; + set + { + _mySqlConnectionStringOptionsValidator.EnsureMandatoryOptions(ref value); + base.ConnectionString = value; + + _dataSource = null; + } + } + + public override void SetDbConnection(DbConnection value, bool contextOwnsConnection) + { + _mySqlConnectionStringOptionsValidator.EnsureMandatoryOptions(value); + + base.SetDbConnection(value, contextOwnsConnection); + } + + [AllowNull] + public new virtual SingleStoreConnection DbConnection + { + get => (SingleStoreConnection)base.DbConnection; + set + { + base.DbConnection = value; + + _dataSource = null; + } + } + + public virtual DbDataSource DbDataSource + { + get => _dataSource; + set + { + _mySqlConnectionStringOptionsValidator.EnsureMandatoryOptions(value); + + if (value is not null) + { + DbConnection = null; + ConnectionString = null; + } + + _dataSource = value; + } + } public virtual ISingleStoreRelationalConnection CreateMasterConnection() { + if (Dependencies.ContextOptions.FindExtension() is not { } mySqlOptions) + { + throw new InvalidOperationException($"{nameof(SingleStoreOptionsExtension)} not found in {nameof(CreateMasterConnection)}"); + } + // Add master connection specific options. var csb = new SingleStoreConnectionStringBuilder(ConnectionString!) { @@ -48,32 +164,29 @@ public virtual ISingleStoreRelationalConnection CreateMasterConnection() csb = AddConnectionStringOptions(csb); - var connectionString = csb.ConnectionString; - var relationalOptions = RelationalOptionsExtension.Extract(Dependencies.ContextOptions); + var masterConnectionString = csb.ConnectionString; // Apply modified connection string. - relationalOptions = relationalOptions.Connection is null - ? relationalOptions.WithConnectionString(connectionString) - : relationalOptions.WithConnection(DbConnection.CloneWith(connectionString)); + var masterMySqlOptions = _dataSource is not null + ? mySqlOptions.WithConnection(((SingleStoreConnection)CreateDbConnection()).CloneWith(masterConnectionString)) + : mySqlOptions.Connection is null + ? mySqlOptions.WithConnectionString(masterConnectionString) + : mySqlOptions.WithConnection(DbConnection.CloneWith(masterConnectionString)); var optionsBuilder = new DbContextOptionsBuilder(); var optionsBuilderInfrastructure = (IDbContextOptionsBuilderInfrastructure)optionsBuilder; - optionsBuilderInfrastructure.AddOrUpdateExtension(relationalOptions); + optionsBuilderInfrastructure.AddOrUpdateExtension(masterMySqlOptions); - return new SingleStoreRelationalConnection(Dependencies with { ContextOptions = optionsBuilder.Options }) + return new SingleStoreRelationalConnection( + Dependencies with { ContextOptions = optionsBuilder.Options }, + _mySqlConnectionStringOptionsValidator, + dataSource: null) { IsMasterConnection = true }; } - [AllowNull] - public new virtual SingleStoreConnection DbConnection - { - get => (SingleStoreConnection)base.DbConnection; - set => base.DbConnection = value; - } - protected virtual SingleStoreConnectionStringBuilder AddConnectionStringOptions(SingleStoreConnectionStringBuilder builder) { var program_version = Assembly.GetExecutingAssembly().GetName().Version.ToString(); diff --git a/src/EFCore.SingleStore/Storage/Internal/SingleStoreSByteTypeMapping.cs b/src/EFCore.SingleStore/Storage/Internal/SingleStoreSByteTypeMapping.cs new file mode 100644 index 000000000..2ff198c23 --- /dev/null +++ b/src/EFCore.SingleStore/Storage/Internal/SingleStoreSByteTypeMapping.cs @@ -0,0 +1,28 @@ +// Copyright (c) Pomelo Foundation. All rights reserved. +// Copyright (c) SingleStore Inc. All rights reserved. +// Licensed under the MIT. See LICENSE in the project root for license information. + +using System.Data; +using Microsoft.EntityFrameworkCore.Storage; + +namespace EntityFrameworkCore.SingleStore.Storage.Internal; + +public class SingleStoreSByteTypeMapping : SByteTypeMapping +{ + public static new SingleStoreSByteTypeMapping Default { get; } = new("tinyint"); + + public SingleStoreSByteTypeMapping( + string storeType, + DbType? dbType = System.Data.DbType.SByte) + : base(storeType, dbType) + { + } + + protected SingleStoreSByteTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new SingleStoreSByteTypeMapping(parameters); +} diff --git a/src/EFCore.SingleStore/Storage/Internal/SingleStoreShortTypeMapping.cs b/src/EFCore.SingleStore/Storage/Internal/SingleStoreShortTypeMapping.cs new file mode 100644 index 000000000..f953f2b2a --- /dev/null +++ b/src/EFCore.SingleStore/Storage/Internal/SingleStoreShortTypeMapping.cs @@ -0,0 +1,28 @@ +// Copyright (c) Pomelo Foundation. All rights reserved. +// Copyright (c) SingleStore Inc. All rights reserved. +// Licensed under the MIT. See LICENSE in the project root for license information. + +using System.Data; +using Microsoft.EntityFrameworkCore.Storage; + +namespace EntityFrameworkCore.SingleStore.Storage.Internal; + +public class SingleStoreShortTypeMapping : ShortTypeMapping +{ + public static new SingleStoreShortTypeMapping Default { get; } = new("smallint"); + + public SingleStoreShortTypeMapping( + string storeType, + DbType? dbType = System.Data.DbType.Int16) + : base(storeType, dbType) + { + } + + protected SingleStoreShortTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new SingleStoreShortTypeMapping(parameters); +} diff --git a/src/EFCore.SingleStore/Storage/Internal/SingleStoreStringTypeMapping.cs b/src/EFCore.SingleStore/Storage/Internal/SingleStoreStringTypeMapping.cs index bb2ace671..35852391b 100644 --- a/src/EFCore.SingleStore/Storage/Internal/SingleStoreStringTypeMapping.cs +++ b/src/EFCore.SingleStore/Storage/Internal/SingleStoreStringTypeMapping.cs @@ -3,12 +3,15 @@ // Licensed under the MIT. See LICENSE in the project root for license information. using System; +using System.Collections.Generic; using System.Data.Common; +using System.Linq; using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Design.Internal; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Utilities; +using Microsoft.EntityFrameworkCore.Storage.Json; using SingleStoreConnector; -using EntityFrameworkCore.SingleStore.Infrastructure.Internal; namespace EntityFrameworkCore.SingleStore.Storage.Internal { @@ -16,32 +19,39 @@ namespace EntityFrameworkCore.SingleStore.Storage.Internal /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// - public class SingleStoreStringTypeMapping : SingleStoreTypeMapping + public class SingleStoreStringTypeMapping : SingleStoreTypeMapping, ISingleStoreCSharpRuntimeAnnotationTypeMappingCodeGenerator { - private readonly bool _forceToString; + public static SingleStoreStringTypeMapping Default { get; } = new("varchar", StoreTypePostfix.Size); + private const int UnicodeMax = 4000; private const int AnsiMax = 8000; private readonly int _maxSpecificSize; - private readonly ISingleStoreOptions _options; + public virtual bool NoBackslashEscapes { get; } + public virtual bool ReplaceLineBreaksWithCharFunction { get; } public virtual bool IsUnquoted { get; } + public virtual bool ForceToString { get; } + public virtual bool IsNationalChar => StoreTypeNameBase.StartsWith("n", StringComparison.OrdinalIgnoreCase) && StoreTypeNameBase.Contains("char", StringComparison.OrdinalIgnoreCase); public SingleStoreStringTypeMapping( [NotNull] string storeType, - ISingleStoreOptions options, StoreTypePostfix storeTypePostfix, bool unicode = true, int? size = null, bool fixedLength = false, + bool noBackslashEscapes = false, + bool replaceLineBreaksWithCharFunction = true, bool unquoted = false, bool forceToString = false) : this( new RelationalTypeMappingParameters( - new CoreTypeMappingParameters(typeof(string)), + new CoreTypeMappingParameters( + typeof(string), + jsonValueReaderWriter: JsonStringReaderWriter.Instance), storeType, storeTypePostfix, unicode @@ -57,7 +67,8 @@ public SingleStoreStringTypeMapping( fixedLength ? SingleStoreDbType.String : SingleStoreDbType.VarString, - options, + noBackslashEscapes, + replaceLineBreaksWithCharFunction, unquoted, forceToString) { @@ -70,14 +81,16 @@ public SingleStoreStringTypeMapping( protected SingleStoreStringTypeMapping( RelationalTypeMappingParameters parameters, SingleStoreDbType mySqlDbType, - ISingleStoreOptions options, + bool noBackslashEscapes, + bool replaceLineBreaksWithCharFunction, bool isUnquoted, bool forceToString) : base(parameters, mySqlDbType) { _maxSpecificSize = CalculateSize(parameters.Unicode, parameters.Size); - _options = options; - _forceToString = forceToString; + NoBackslashEscapes = noBackslashEscapes; + ReplaceLineBreaksWithCharFunction = replaceLineBreaksWithCharFunction; + ForceToString = forceToString; IsUnquoted = isUnquoted; } @@ -92,10 +105,20 @@ private static int CalculateSize(bool unicode, int? size) /// The parameters for this mapping. /// The newly created mapping. protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new SingleStoreStringTypeMapping(parameters, SingleStoreDbType, _options, IsUnquoted, _forceToString); + => new SingleStoreStringTypeMapping(parameters, SingleStoreDbType, NoBackslashEscapes, ReplaceLineBreaksWithCharFunction, IsUnquoted, ForceToString); - public virtual RelationalTypeMapping Clone(bool? unquoted = null, bool? forceToString = null) - => new SingleStoreStringTypeMapping(Parameters, SingleStoreDbType, _options, unquoted ?? IsUnquoted, forceToString ?? _forceToString); + public virtual RelationalTypeMapping Clone( + bool? unquoted = null, + bool? forceToString = null, + bool? noBackslashEscapes = null, + bool? replaceLineBreaksWithCharFunction = null) + => new SingleStoreStringTypeMapping( + Parameters, + SingleStoreDbType, + noBackslashEscapes ?? NoBackslashEscapes, + replaceLineBreaksWithCharFunction ?? ReplaceLineBreaksWithCharFunction, + unquoted ?? IsUnquoted, + forceToString ?? ForceToString); /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used @@ -109,7 +132,7 @@ protected override void ConfigureParameter(DbParameter parameter) // -1 (unbounded) to avoid size inference. var value = parameter.Value; - if (_forceToString && value != null && value != DBNull.Value) + if (ForceToString && value != null && value != DBNull.Value) { value = value.ToString(); } @@ -140,13 +163,13 @@ protected override void ConfigureParameter(DbParameter parameter) protected override string GenerateNonNullSqlLiteral(object value) { - var stringValue = _forceToString + var stringValue = ForceToString ? value.ToString() : (string)value; return IsUnquoted - ? EscapeSqlLiteral(stringValue, !_options.NoBackslashEscapes) - : EscapeSqlLiteralWithLineBreaks(stringValue, !_options.NoBackslashEscapes, _options.ReplaceLineBreaksWithCharFunction); + ? EscapeSqlLiteral(stringValue, !NoBackslashEscapes) + : EscapeSqlLiteralWithLineBreaks(stringValue, !NoBackslashEscapes, ReplaceLineBreaksWithCharFunction); } public static string EscapeSqlLiteralWithLineBreaks(string value, bool escapeBackslashes, bool replaceLineBreaksWithCharFunction) @@ -180,5 +203,65 @@ public static string EscapeBackslashes(string literal, bool escapeBackslashes) ? literal.Replace(@"\", @"\\") : literal; } + + void ISingleStoreCSharpRuntimeAnnotationTypeMappingCodeGenerator.Create( + CSharpRuntimeAnnotationCodeGeneratorParameters codeGeneratorParameters, + CSharpRuntimeAnnotationCodeGeneratorDependencies codeGeneratorDependencies) + { + var defaultTypeMapping = Default; + if (defaultTypeMapping == this) + { + return; + } + + var code = codeGeneratorDependencies.CSharpHelper; + + var cloneParameters = new List(); + + if (IsUnquoted != defaultTypeMapping.IsUnquoted) + { + cloneParameters.Add($"unquoted: {code.Literal(IsUnquoted)}"); + } + + if (ForceToString != defaultTypeMapping.ForceToString) + { + cloneParameters.Add($"forceToString: {code.Literal(ForceToString)}"); + } + + if (NoBackslashEscapes != defaultTypeMapping.NoBackslashEscapes) + { + cloneParameters.Add($"noBackslashEscapes: {code.Literal(NoBackslashEscapes)}"); + } + + if (ReplaceLineBreaksWithCharFunction != defaultTypeMapping.ReplaceLineBreaksWithCharFunction) + { + cloneParameters.Add($"replaceLineBreaksWithCharFunction: {code.Literal(ReplaceLineBreaksWithCharFunction)}"); + } + + if (cloneParameters.Any()) + { + var mainBuilder = codeGeneratorParameters.MainBuilder; + + mainBuilder.AppendLine(";"); + + mainBuilder + .AppendLine($"{codeGeneratorParameters.TargetName}.TypeMapping = (({code.Reference(GetType())}){codeGeneratorParameters.TargetName}.TypeMapping).Clone(") + .IncrementIndent(); + + for (var i = 0; i < cloneParameters.Count; i++) + { + if (i > 0) + { + mainBuilder.AppendLine(","); + } + + mainBuilder.Append(cloneParameters[i]); + } + + mainBuilder + .Append(")") + .DecrementIndent(); + } + } } } diff --git a/src/EFCore.SingleStore/Storage/Internal/SingleStoreTimeTypeMapping.cs b/src/EFCore.SingleStore/Storage/Internal/SingleStoreTimeTypeMapping.cs index f13466d1d..af94921e2 100644 --- a/src/EFCore.SingleStore/Storage/Internal/SingleStoreTimeTypeMapping.cs +++ b/src/EFCore.SingleStore/Storage/Internal/SingleStoreTimeTypeMapping.cs @@ -6,6 +6,7 @@ using System.Globalization; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace EntityFrameworkCore.SingleStore.Storage.Internal { @@ -19,6 +20,8 @@ public class SingleStoreTimeTypeMapping : RelationalTypeMapping, IDefaultValueCo { private readonly bool _isDefaultValueCompatible; + public static SingleStoreTimeTypeMapping Default { get; } = new("time", typeof(TimeOnly)); + /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. @@ -30,7 +33,9 @@ public SingleStoreTimeTypeMapping( bool isDefaultValueCompatible = true) : this( new RelationalTypeMappingParameters( - new CoreTypeMappingParameters(clrType), + new CoreTypeMappingParameters( + clrType, + jsonValueReaderWriter: JsonTimeOnlyReaderWriter.Instance), storeType, StoreTypePostfix.Precision, System.Data.DbType.Time, diff --git a/src/EFCore.SingleStore/Storage/Internal/SingleStoreTypeMapping.cs b/src/EFCore.SingleStore/Storage/Internal/SingleStoreTypeMapping.cs index cb644ee72..0c2ebfc6d 100644 --- a/src/EFCore.SingleStore/Storage/Internal/SingleStoreTypeMapping.cs +++ b/src/EFCore.SingleStore/Storage/Internal/SingleStoreTypeMapping.cs @@ -8,6 +8,7 @@ using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Json; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using SingleStoreConnector; @@ -21,7 +22,7 @@ namespace EntityFrameworkCore.SingleStore.Storage.Internal public abstract class SingleStoreTypeMapping : RelationalTypeMapping { /// - /// The database type used by SingleStore. + /// The database type used by SingleStore Distributed. /// public virtual SingleStoreDbType SingleStoreDbType { get; } @@ -34,10 +35,20 @@ public SingleStoreTypeMapping( bool unicode = false, int? size = null, ValueConverter valueConverter = null, - ValueComparer valueComparer = null) + ValueComparer valueComparer = null, + JsonValueReaderWriter jsonValueReaderWriter = null) : base( new RelationalTypeMappingParameters( - new CoreTypeMappingParameters(clrType, valueConverter, valueComparer), storeType, StoreTypePostfix.None, dbType, unicode, size)) + new CoreTypeMappingParameters( + clrType, + valueConverter, + valueComparer, + jsonValueReaderWriter: jsonValueReaderWriter), + storeType, + StoreTypePostfix.None, + dbType, + unicode, + size)) => SingleStoreDbType = mySqlDbType; /// diff --git a/src/EFCore.SingleStore/Storage/Internal/SingleStoreTypeMappingSource.cs b/src/EFCore.SingleStore/Storage/Internal/SingleStoreTypeMappingSource.cs index ff64cdcc0..0e9faa828 100644 --- a/src/EFCore.SingleStore/Storage/Internal/SingleStoreTypeMappingSource.cs +++ b/src/EFCore.SingleStore/Storage/Internal/SingleStoreTypeMappingSource.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; -using System.Data; using System.Linq; using EntityFrameworkCore.SingleStore.Infrastructure.Internal; using JetBrains.Annotations; @@ -20,29 +19,29 @@ public class SingleStoreTypeMappingSource : RelationalTypeMappingSource { // boolean private readonly SingleStoreBoolTypeMapping _bit1 = new SingleStoreBoolTypeMapping("bit", size: 1); - private readonly SingleStoreBoolTypeMapping _tinyint1 = new SingleStoreBoolTypeMapping("tinyint", size: 1); + private readonly SingleStoreBoolTypeMapping _tinyint1 = SingleStoreBoolTypeMapping.Default; // bit - private readonly ULongTypeMapping _bit = new ULongTypeMapping("bit", DbType.UInt64); + private readonly SingleStoreULongTypeMapping _bit = new SingleStoreULongTypeMapping("bit"); // integers - private readonly SByteTypeMapping _tinyint = new SByteTypeMapping("tinyint", DbType.SByte); - private readonly ByteTypeMapping _utinyint = new ByteTypeMapping("tinyint unsigned", DbType.Byte); - private readonly ShortTypeMapping _smallint = new ShortTypeMapping("smallint", DbType.Int16); - private readonly UShortTypeMapping _usmallint = new UShortTypeMapping("smallint unsigned", DbType.UInt16); - private readonly IntTypeMapping _int = new IntTypeMapping("int", DbType.Int32); - private readonly UIntTypeMapping _uint = new UIntTypeMapping("int unsigned", DbType.UInt32); - private readonly LongTypeMapping _bigint = new LongTypeMapping("bigint", DbType.Int64); - private readonly ULongTypeMapping _ubigint = new ULongTypeMapping("bigint unsigned", DbType.UInt64); + private readonly SingleStoreSByteTypeMapping _tinyint = SingleStoreSByteTypeMapping.Default; + private readonly SingleStoreByteTypeMapping _utinyint = SingleStoreByteTypeMapping.Default; + private readonly SingleStoreShortTypeMapping _smallint = SingleStoreShortTypeMapping.Default; + private readonly SingleStoreUShortTypeMapping _usmallint = SingleStoreUShortTypeMapping.Default; + private readonly SingleStoreIntTypeMapping _int = SingleStoreIntTypeMapping.Default; + private readonly SingleStoreUIntTypeMapping _uint = SingleStoreUIntTypeMapping.Default; + private readonly SingleStoreLongTypeMapping _bigint = SingleStoreLongTypeMapping.Default; + private readonly SingleStoreULongTypeMapping _ubigint = SingleStoreULongTypeMapping.Default; // decimals - private readonly SingleStoreDecimalTypeMapping _decimal = new SingleStoreDecimalTypeMapping("decimal", precision: 65, scale: 30); - private readonly SingleStoreDoubleTypeMapping _double = new SingleStoreDoubleTypeMapping("double", DbType.Double); - private readonly SingleStoreFloatTypeMapping _float = new SingleStoreFloatTypeMapping("float", DbType.Single); + private readonly SingleStoreDecimalTypeMapping _decimal = SingleStoreDecimalTypeMapping.Default; + private readonly SingleStoreDoubleTypeMapping _double = SingleStoreDoubleTypeMapping.Default; + private readonly SingleStoreFloatTypeMapping _float = SingleStoreFloatTypeMapping.Default; // binary private readonly RelationalTypeMapping _binary = new SingleStoreByteArrayTypeMapping(fixedLength: true); - private readonly RelationalTypeMapping _varbinary = new SingleStoreByteArrayTypeMapping(); + private readonly RelationalTypeMapping _varbinary = SingleStoreByteArrayTypeMapping.Default; // // String mappings depend on the SingleStoreOptions.NoBackslashEscapes setting: @@ -61,14 +60,14 @@ public class SingleStoreTypeMappingSource : RelationalTypeMappingSource private SingleStoreStringTypeMapping _enum; // DateTime - private readonly SingleStoreYearTypeMapping _year = new SingleStoreYearTypeMapping("year"); - private readonly SingleStoreDateTypeMapping _dateDateOnly = new SingleStoreDateTypeMapping("date", typeof(DateOnly)); + private readonly SingleStoreYearTypeMapping _year = SingleStoreYearTypeMapping.Default; + private readonly SingleStoreDateTypeMapping _dateDateOnly = SingleStoreDateTypeMapping.Default; private readonly SingleStoreDateTypeMapping _dateDateTime = new SingleStoreDateTypeMapping("date", typeof(DateTime)); - private readonly SingleStoreTimeTypeMapping _timeTimeOnly = new SingleStoreTimeTypeMapping("time", typeof(TimeOnly)); + private readonly SingleStoreTimeTypeMapping _timeTimeOnly = SingleStoreTimeTypeMapping.Default; private readonly SingleStoreTimeTypeMapping _timeTimeSpan = new SingleStoreTimeTypeMapping("time", typeof(TimeSpan)); - private readonly SingleStoreDateTimeTypeMapping _dateTime = new SingleStoreDateTimeTypeMapping("datetime"); + private readonly SingleStoreDateTimeTypeMapping _dateTime = SingleStoreDateTimeTypeMapping.Default; private readonly SingleStoreDateTimeTypeMapping _timeStamp = new SingleStoreDateTimeTypeMapping("timestamp"); - private readonly SingleStoreDateTimeOffsetTypeMapping _dateTimeOffset = new SingleStoreDateTimeOffsetTypeMapping("datetime"); + private readonly SingleStoreDateTimeOffsetTypeMapping _dateTimeOffset = SingleStoreDateTimeOffsetTypeMapping.Default; private readonly SingleStoreDateTimeOffsetTypeMapping _timeStampOffset = new SingleStoreDateTimeOffsetTypeMapping("timestamp"); private readonly RelationalTypeMapping _binaryRowVersion @@ -93,8 +92,8 @@ private readonly RelationalTypeMapping _binaryRowVersion6 private SingleStoreJsonTypeMapping _jsonDefaultString; // Scaffolding type mappings - private readonly SingleStoreCodeGenerationMemberAccessTypeMapping _codeGenerationMemberAccess = new SingleStoreCodeGenerationMemberAccessTypeMapping(); - private readonly SingleStoreCodeGenerationServerVersionCreationTypeMapping _codeGenerationServerVersionCreation = new SingleStoreCodeGenerationServerVersionCreationTypeMapping(); + private readonly SingleStoreCodeGenerationMemberAccessTypeMapping _codeGenerationMemberAccess = SingleStoreCodeGenerationMemberAccessTypeMapping.Default; + private readonly SingleStoreCodeGenerationServerVersionCreationTypeMapping _codeGenerationServerVersionCreation = SingleStoreCodeGenerationServerVersionCreationTypeMapping.Default; private Dictionary _storeTypeMappings; private Dictionary _clrTypeMappings; @@ -120,23 +119,23 @@ private void Initialize() // String mappings depend on the SingleStoreOptions.NoBackslashEscapes setting: // - _charUnicode = new SingleStoreStringTypeMapping("char", _options, StoreTypePostfix.Size, fixedLength: true); - _varcharUnicode = new SingleStoreStringTypeMapping("varchar", _options, StoreTypePostfix.Size); - _tinytextUnicode = new SingleStoreStringTypeMapping("tinytext", _options, StoreTypePostfix.None); - _textUnicode = new SingleStoreStringTypeMapping("text", _options, StoreTypePostfix.None); - _mediumtextUnicode = new SingleStoreStringTypeMapping("mediumtext", _options, StoreTypePostfix.None); - _longtextUnicode = new SingleStoreStringTypeMapping("longtext", _options, StoreTypePostfix.None); + _charUnicode = new SingleStoreStringTypeMapping("char", StoreTypePostfix.Size, fixedLength: true, noBackslashEscapes: _options.NoBackslashEscapes, replaceLineBreaksWithCharFunction: _options.ReplaceLineBreaksWithCharFunction); + _varcharUnicode = new SingleStoreStringTypeMapping("varchar", StoreTypePostfix.Size, noBackslashEscapes: _options.NoBackslashEscapes, replaceLineBreaksWithCharFunction: _options.ReplaceLineBreaksWithCharFunction); + _tinytextUnicode = new SingleStoreStringTypeMapping("tinytext", StoreTypePostfix.None, noBackslashEscapes: _options.NoBackslashEscapes, replaceLineBreaksWithCharFunction: _options.ReplaceLineBreaksWithCharFunction); + _textUnicode = new SingleStoreStringTypeMapping("text", StoreTypePostfix.None, noBackslashEscapes: _options.NoBackslashEscapes, replaceLineBreaksWithCharFunction: _options.ReplaceLineBreaksWithCharFunction); + _mediumtextUnicode = new SingleStoreStringTypeMapping("mediumtext", StoreTypePostfix.None, noBackslashEscapes: _options.NoBackslashEscapes, replaceLineBreaksWithCharFunction: _options.ReplaceLineBreaksWithCharFunction); + _longtextUnicode = new SingleStoreStringTypeMapping("longtext", StoreTypePostfix.None, noBackslashEscapes: _options.NoBackslashEscapes, replaceLineBreaksWithCharFunction: _options.ReplaceLineBreaksWithCharFunction); - _nchar = new SingleStoreStringTypeMapping("nchar", _options, StoreTypePostfix.Size, fixedLength: true); - _nvarchar = new SingleStoreStringTypeMapping("nvarchar", _options, StoreTypePostfix.Size); + _nchar = new SingleStoreStringTypeMapping("nchar", StoreTypePostfix.Size, fixedLength: true, noBackslashEscapes: _options.NoBackslashEscapes, replaceLineBreaksWithCharFunction: _options.ReplaceLineBreaksWithCharFunction); + _nvarchar = new SingleStoreStringTypeMapping("nvarchar", StoreTypePostfix.Size, noBackslashEscapes: _options.NoBackslashEscapes, replaceLineBreaksWithCharFunction: _options.ReplaceLineBreaksWithCharFunction); - _enum = new SingleStoreStringTypeMapping("enum", _options, StoreTypePostfix.None); + _enum = new SingleStoreStringTypeMapping("enum", StoreTypePostfix.None, noBackslashEscapes: _options.NoBackslashEscapes, replaceLineBreaksWithCharFunction: _options.ReplaceLineBreaksWithCharFunction); _guid = SingleStoreGuidTypeMapping.IsValidGuidFormat(_options.ConnectionSettings.GuidFormat) ? new SingleStoreGuidTypeMapping(_options.ConnectionSettings.GuidFormat) : null; - _jsonDefaultString = new SingleStoreJsonTypeMapping("json", null, null, _options); + _jsonDefaultString = new SingleStoreJsonTypeMapping("json", null, null, _options.NoBackslashEscapes, _options.ReplaceLineBreaksWithCharFunction); _storeTypeMappings = new Dictionary(StringComparer.OrdinalIgnoreCase) @@ -227,24 +226,24 @@ private void Initialize() // datetimes { typeof(DateOnly), _dateDateOnly }, - { typeof(TimeOnly), _timeTimeOnly.Clone(_options.DefaultDataTypeMappings.ClrTimeOnlyPrecision, null) }, + { typeof(TimeOnly), _timeTimeOnly.WithPrecisionAndScale(_options.DefaultDataTypeMappings.ClrTimeOnlyPrecision, null) }, { typeof(TimeSpan), _options.DefaultDataTypeMappings.ClrTimeSpan switch { - SingleStoreTimeSpanType.Time6 => _timeTimeSpan.Clone(6, null), + SingleStoreTimeSpanType.Time6 => _timeTimeSpan.WithPrecisionAndScale(6, null), SingleStoreTimeSpanType.Time => _timeTimeSpan, _ => _timeTimeSpan }}, { typeof(DateTime), _options.DefaultDataTypeMappings.ClrDateTime switch { - SingleStoreDateTimeType.DateTime6 =>_dateTime.Clone(6, null), - SingleStoreDateTimeType.Timestamp6 => _timeStamp.Clone(6, null), + SingleStoreDateTimeType.DateTime6 =>_dateTime.WithPrecisionAndScale(6, null), + SingleStoreDateTimeType.Timestamp6 => _timeStamp.WithPrecisionAndScale(6, null), SingleStoreDateTimeType.Timestamp => _timeStamp, _ => _dateTime, }}, { typeof(DateTimeOffset), _options.DefaultDataTypeMappings.ClrDateTimeOffset switch { - SingleStoreDateTimeType.DateTime6 =>_dateTimeOffset.Clone(6, null), - SingleStoreDateTimeType.Timestamp6 => _timeStampOffset.Clone(6, null), + SingleStoreDateTimeType.DateTime6 =>_dateTimeOffset.WithPrecisionAndScale(6, null), + SingleStoreDateTimeType.Timestamp6 => _timeStampOffset.WithPrecisionAndScale(6, null), SingleStoreDateTimeType.Timestamp => _timeStampOffset, _ => _dateTimeOffset, }}, @@ -327,9 +326,9 @@ private RelationalTypeMapping FindRawMapping(RelationalTypeMappingInfo mappingIn { return clrType == null ? mappings[0] - .Clone(in mappingInfo) + .WithTypeMappingInfo(in mappingInfo) : mappings.FirstOrDefault(m => m.ClrType == clrType) - ?.Clone(in mappingInfo); + ?.WithTypeMappingInfo(in mappingInfo); } if (storeTypeName.Equals("json", StringComparison.OrdinalIgnoreCase) && @@ -351,14 +350,14 @@ private RelationalTypeMapping FindRawMapping(RelationalTypeMappingInfo mappingIn { if (clrType == typeof(decimal)) { - return mapping.Clone(mappingInfo.Precision.Value, mappingInfo.Scale); + return mapping.WithPrecisionAndScale(mappingInfo.Precision.Value, mappingInfo.Scale); } if (clrType == typeof(DateTime) || clrType == typeof(DateTimeOffset) || clrType == typeof(TimeSpan)) { - return mapping.Clone(mappingInfo.Precision.Value, null); + return mapping.WithPrecisionAndScale(mappingInfo.Precision.Value, null); } } @@ -384,11 +383,15 @@ private RelationalTypeMapping FindRawMapping(RelationalTypeMappingInfo mappingIn // If a string column size is bigger than it can/might be, we automatically adjust it to a variable one with an // unlimited size. - if (size > 65_553 / _options.DefaultCharSet.MaxBytesPerChar) + if (size > 65_535 / _options.DefaultCharSet.MaxBytesPerChar) { size = null; isFixedLength = false; } + else if (size < 0) // specifying HasMaxLength(-1) is valid and should lead to an unbounded string/text. + { + size = null; + } mapping = isFixedLength ? _charUnicode @@ -398,7 +401,7 @@ private RelationalTypeMapping FindRawMapping(RelationalTypeMappingInfo mappingIn return size == null ? mapping - : mapping.Clone($"{mapping.StoreTypeNameBase}({size})", size); + : mapping.WithStoreTypeAndSize($"{mapping.StoreTypeNameBase}({size})", size); } if (clrType == typeof(byte[])) @@ -415,6 +418,12 @@ private RelationalTypeMapping FindRawMapping(RelationalTypeMappingInfo mappingIn ? _options.ServerVersion.MaxKeyLength : (int?)null); + // Specifying HasMaxLength(-1) is valid and should lead to an unbounded byte array/blob. + if (size < 0) + { + size = null; + } + return new SingleStoreByteArrayTypeMapping( size: size, fixedLength: mappingInfo.IsFixedLength == true); @@ -429,9 +438,14 @@ private RelationalTypeMapping FindRawMapping(RelationalTypeMappingInfo mappingIn return null; } - protected override string ParseStoreTypeName(string storeTypeName, out bool? unicode, out int? size, out int? precision, out int? scale) + protected override string ParseStoreTypeName( + string storeTypeName, + ref bool? unicode, + ref int? size, + ref int? precision, + ref int? scale) { - var storeTypeBaseName = base.ParseStoreTypeName(storeTypeName, out unicode, out size, out precision, out scale); + var storeTypeBaseName = base.ParseStoreTypeName(storeTypeName, ref unicode, ref size, ref precision, ref scale); if (storeTypeBaseName is not null) { diff --git a/src/EFCore.SingleStore/Storage/Internal/SingleStoreUIntTypeMapping.cs b/src/EFCore.SingleStore/Storage/Internal/SingleStoreUIntTypeMapping.cs new file mode 100644 index 000000000..7326d5a60 --- /dev/null +++ b/src/EFCore.SingleStore/Storage/Internal/SingleStoreUIntTypeMapping.cs @@ -0,0 +1,28 @@ +// Copyright (c) Pomelo Foundation. All rights reserved. +// Copyright (c) SingleStore Inc. All rights reserved. +// Licensed under the MIT. See LICENSE in the project root for license information. + +using System.Data; +using Microsoft.EntityFrameworkCore.Storage; + +namespace EntityFrameworkCore.SingleStore.Storage.Internal; + +public class SingleStoreUIntTypeMapping : UIntTypeMapping +{ + public static new SingleStoreUIntTypeMapping Default { get; } = new("int unsigned"); + + public SingleStoreUIntTypeMapping( + string storeType, + DbType? dbType = System.Data.DbType.UInt32) + : base(storeType, dbType) + { + } + + protected SingleStoreUIntTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new SingleStoreUIntTypeMapping(parameters); +} diff --git a/src/EFCore.SingleStore/Storage/Internal/SingleStoreULongTypeMapping.cs b/src/EFCore.SingleStore/Storage/Internal/SingleStoreULongTypeMapping.cs new file mode 100644 index 000000000..0633000d3 --- /dev/null +++ b/src/EFCore.SingleStore/Storage/Internal/SingleStoreULongTypeMapping.cs @@ -0,0 +1,28 @@ +// Copyright (c) Pomelo Foundation. All rights reserved. +// Copyright (c) SingleStore Inc. All rights reserved. +// Licensed under the MIT. See LICENSE in the project root for license information. + +using System.Data; +using Microsoft.EntityFrameworkCore.Storage; + +namespace EntityFrameworkCore.SingleStore.Storage.Internal; + +public class SingleStoreULongTypeMapping : ULongTypeMapping +{ + public static new SingleStoreULongTypeMapping Default { get; } = new("bigint unsigned"); + + public SingleStoreULongTypeMapping( + string storeType, + DbType? dbType = System.Data.DbType.UInt64) + : base(storeType, dbType) + { + } + + protected SingleStoreULongTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new SingleStoreULongTypeMapping(parameters); +} diff --git a/src/EFCore.SingleStore/Storage/Internal/SingleStoreUShortTypeMapping.cs b/src/EFCore.SingleStore/Storage/Internal/SingleStoreUShortTypeMapping.cs new file mode 100644 index 000000000..fe236a147 --- /dev/null +++ b/src/EFCore.SingleStore/Storage/Internal/SingleStoreUShortTypeMapping.cs @@ -0,0 +1,28 @@ +// Copyright (c) Pomelo Foundation. All rights reserved. +// Copyright (c) SingleStore Inc. All rights reserved. +// Licensed under the MIT. See LICENSE in the project root for license information. + +using System.Data; +using Microsoft.EntityFrameworkCore.Storage; + +namespace EntityFrameworkCore.SingleStore.Storage.Internal; + +public class SingleStoreUShortTypeMapping : UShortTypeMapping +{ + public static new SingleStoreUShortTypeMapping Default { get; } = new("smallint unsigned"); + + public SingleStoreUShortTypeMapping( + string storeType, + DbType? dbType = System.Data.DbType.UInt16) + : base(storeType, dbType) + { + } + + protected SingleStoreUShortTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new SingleStoreUShortTypeMapping(parameters); +} diff --git a/src/EFCore.SingleStore/Storage/Internal/SingleStoreYearTypeMapping.cs b/src/EFCore.SingleStore/Storage/Internal/SingleStoreYearTypeMapping.cs index 9814ee134..757cff412 100644 --- a/src/EFCore.SingleStore/Storage/Internal/SingleStoreYearTypeMapping.cs +++ b/src/EFCore.SingleStore/Storage/Internal/SingleStoreYearTypeMapping.cs @@ -4,18 +4,22 @@ using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Json; using SingleStoreConnector; namespace EntityFrameworkCore.SingleStore.Storage.Internal { public class SingleStoreYearTypeMapping : SingleStoreTypeMapping { + public static SingleStoreYearTypeMapping Default { get; } = new("year"); + public SingleStoreYearTypeMapping([NotNull] string storeType) : base( storeType, typeof(short), SingleStoreDbType.Year, - System.Data.DbType.Int16) + System.Data.DbType.Int16, + jsonValueReaderWriter: JsonInt16ReaderWriter.Instance) { } diff --git a/src/EFCore.SingleStore/Update/Internal/SingleStoreUpdateSqlGenerator.cs b/src/EFCore.SingleStore/Update/Internal/SingleStoreUpdateSqlGenerator.cs index b9c70f4a3..30dee7d35 100644 --- a/src/EFCore.SingleStore/Update/Internal/SingleStoreUpdateSqlGenerator.cs +++ b/src/EFCore.SingleStore/Update/Internal/SingleStoreUpdateSqlGenerator.cs @@ -215,7 +215,7 @@ private static (string tableName, string schema) GetTableNameAndSchema(IColumnMo { // CHECK: Is this branch ever hit and then returns something different than null, or can we just rely on // `modification.Column?.Table`? - return (property.DeclaringEntityType.GetTableName(), property.DeclaringEntityType.GetSchema()); + return (property.DeclaringType.GetTableName(), property.DeclaringType.GetSchema()); } } } diff --git a/src/EFCore.SingleStore/Utilities/ByteArrayFormatter.cs b/src/EFCore.SingleStore/Utilities/ByteArrayFormatter.cs deleted file mode 100644 index 7c9cc60cd..000000000 --- a/src/EFCore.SingleStore/Utilities/ByteArrayFormatter.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) Pomelo Foundation. All rights reserved. -// Copyright (c) SingleStore Inc. All rights reserved. -// Licensed under the MIT. See LICENSE in the project root for license information. - -using System.Text; -using JetBrains.Annotations; - -namespace EntityFrameworkCore.SingleStore.Utilities -{ - internal static class ByteArrayFormatter - { - private static readonly char[] _lookup = new char[16] - { - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' - }; - - public static string ToHex([NotNull] byte[] b) - { - if (b.Length == 0) - { - return "X''"; - } - - var builder = new StringBuilder("0x", 2 + (b.Length * 2)); - for (var i = 0; i < b.Length; i++) - { - var b1 = (byte)(b[i] >> 4); - var b2 = (byte)(b[i] & 0xF); - builder.Append(_lookup[b1]); - builder.Append(_lookup[b2]); - } - return builder.ToString(); - } - } -} diff --git a/src/EFCore.SingleStore/ValueGeneration/Internal/SingleStoreValueGeneratorSelector.cs b/src/EFCore.SingleStore/ValueGeneration/Internal/SingleStoreValueGeneratorSelector.cs index 5a80f6b71..b12e8b47a 100644 --- a/src/EFCore.SingleStore/ValueGeneration/Internal/SingleStoreValueGeneratorSelector.cs +++ b/src/EFCore.SingleStore/ValueGeneration/Internal/SingleStoreValueGeneratorSelector.cs @@ -31,17 +31,14 @@ public SingleStoreValueGeneratorSelector( /// 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 override ValueGenerator Create(IProperty property, IEntityType entityType) + protected override ValueGenerator FindForType(IProperty property, ITypeBase typeBase, Type clrType) { - Check.NotNull(property, nameof(property)); - Check.NotNull(entityType, nameof(entityType)); - - var ret = property.ClrType.UnwrapNullableType() == typeof(Guid) + var ret = clrType == typeof(Guid) ? property.ValueGenerated == ValueGenerated.Never || property.GetDefaultValueSql() != null - ? (ValueGenerator)new TemporaryGuidValueGenerator() + ? new TemporaryGuidValueGenerator() : new SingleStoreSequentialGuidValueGenerator(_options) - : base.Create(property, entityType); + : base.FindForType(property, typeBase, clrType); return ret; } } diff --git a/test/Directory.Build.props b/test/Directory.Build.props index 2592c4060..09501c267 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -13,5 +13,6 @@ + diff --git a/test/EFCore.SingleStore.FunctionalTests/AssemblyInfo.cs b/test/EFCore.SingleStore.FunctionalTests/AssemblyInfo.cs index 9205dc0df..6714ac906 100644 --- a/test/EFCore.SingleStore.FunctionalTests/AssemblyInfo.cs +++ b/test/EFCore.SingleStore.FunctionalTests/AssemblyInfo.cs @@ -9,7 +9,7 @@ #if FIXED_TEST_ORDER -[assembly: CollectionBehavior(DisableTestParallelization = true)] +[assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly, DisableTestParallelization = true, MaxParallelThreads = 1)] [assembly: TestCollectionOrderer("EntityFrameworkCore.SingleStore.FunctionalTests.TestUtilities.Xunit.SingleStoreTestCollectionOrderer", "EntityFrameworkCore.SingleStore.FunctionalTests")] [assembly: TestCaseOrderer("EntityFrameworkCore.SingleStore.FunctionalTests.TestUtilities.Xunit.SingleStoreTestCaseOrderer", "EntityFrameworkCore.SingleStore.FunctionalTests")] diff --git a/test/EFCore.SingleStore.FunctionalTests/BuiltInDataTypesSingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/BuiltInDataTypesSingleStoreTest.cs index 012b572ad..97c9d2f83 100644 --- a/test/EFCore.SingleStore.FunctionalTests/BuiltInDataTypesSingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/BuiltInDataTypesSingleStoreTest.cs @@ -1009,6 +1009,7 @@ public virtual void Columns_have_expected_data_types() BuiltInDataTypes.TestBoolean ---> [tinyint] [Precision = 3 Scale = 0] BuiltInDataTypes.TestByte ---> [tinyint] [Precision = 3 Scale = 0] BuiltInDataTypes.TestCharacter ---> [varchar] [MaxLength = 1] +BuiltInDataTypes.TestDateOnly ---> [date] BuiltInDataTypes.TestDateTime ---> [datetime] [Precision = 6] BuiltInDataTypes.TestDateTimeOffset ---> [datetime] [Precision = 6] BuiltInDataTypes.TestDecimal ---> [decimal] [Precision = 65 Scale = 30] @@ -1018,6 +1019,7 @@ public virtual void Columns_have_expected_data_types() BuiltInDataTypes.TestInt64 ---> [bigint] [Precision = 19 Scale = 0] BuiltInDataTypes.TestSignedByte ---> [tinyint] [Precision = 3 Scale = 0] BuiltInDataTypes.TestSingle ---> [float] [Precision = 12] +BuiltInDataTypes.TestTimeOnly ---> [time] [Precision = 6] BuiltInDataTypes.TestTimeSpan ---> [time] BuiltInDataTypes.TestUnsignedInt16 ---> [smallint] [Precision = 5 Scale = 0] BuiltInDataTypes.TestUnsignedInt32 ---> [int] [Precision = 10 Scale = 0] @@ -1035,6 +1037,7 @@ public virtual void Columns_have_expected_data_types() BuiltInDataTypesShadow.TestBoolean ---> [tinyint] [Precision = 3 Scale = 0] BuiltInDataTypesShadow.TestByte ---> [tinyint] [Precision = 3 Scale = 0] BuiltInDataTypesShadow.TestCharacter ---> [varchar] [MaxLength = 1] +BuiltInDataTypesShadow.TestDateOnly ---> [date] BuiltInDataTypesShadow.TestDateTime ---> [datetime] [Precision = 6] BuiltInDataTypesShadow.TestDateTimeOffset ---> [datetime] [Precision = 6] BuiltInDataTypesShadow.TestDecimal ---> [decimal] [Precision = 65 Scale = 30] @@ -1044,6 +1047,7 @@ public virtual void Columns_have_expected_data_types() BuiltInDataTypesShadow.TestInt64 ---> [bigint] [Precision = 19 Scale = 0] BuiltInDataTypesShadow.TestSignedByte ---> [tinyint] [Precision = 3 Scale = 0] BuiltInDataTypesShadow.TestSingle ---> [float] [Precision = 12] +BuiltInDataTypesShadow.TestTimeOnly ---> [time] [Precision = 6] BuiltInDataTypesShadow.TestTimeSpan ---> [time] BuiltInDataTypesShadow.TestUnsignedInt16 ---> [smallint] [Precision = 5 Scale = 0] BuiltInDataTypesShadow.TestUnsignedInt32 ---> [int] [Precision = 10 Scale = 0] @@ -1062,6 +1066,7 @@ public virtual void Columns_have_expected_data_types() BuiltInNullableDataTypes.TestNullableBoolean ---> [nullable tinyint] [Precision = 3 Scale = 0] BuiltInNullableDataTypes.TestNullableByte ---> [nullable tinyint] [Precision = 3 Scale = 0] BuiltInNullableDataTypes.TestNullableCharacter ---> [nullable varchar] [MaxLength = 1] +BuiltInNullableDataTypes.TestNullableDateOnly ---> [nullable date] BuiltInNullableDataTypes.TestNullableDateTime ---> [nullable datetime] [Precision = 6] BuiltInNullableDataTypes.TestNullableDateTimeOffset ---> [nullable datetime] [Precision = 6] BuiltInNullableDataTypes.TestNullableDecimal ---> [nullable decimal] [Precision = 65 Scale = 30] @@ -1071,6 +1076,7 @@ public virtual void Columns_have_expected_data_types() BuiltInNullableDataTypes.TestNullableInt64 ---> [nullable bigint] [Precision = 19 Scale = 0] BuiltInNullableDataTypes.TestNullableSignedByte ---> [nullable tinyint] [Precision = 3 Scale = 0] BuiltInNullableDataTypes.TestNullableSingle ---> [nullable float] [Precision = 12] +BuiltInNullableDataTypes.TestNullableTimeOnly ---> [nullable time] [Precision = 6] BuiltInNullableDataTypes.TestNullableTimeSpan ---> [nullable time] BuiltInNullableDataTypes.TestNullableUnsignedInt16 ---> [nullable smallint] [Precision = 5 Scale = 0] BuiltInNullableDataTypes.TestNullableUnsignedInt32 ---> [nullable int] [Precision = 10 Scale = 0] @@ -1090,6 +1096,7 @@ public virtual void Columns_have_expected_data_types() BuiltInNullableDataTypesShadow.TestNullableBoolean ---> [nullable tinyint] [Precision = 3 Scale = 0] BuiltInNullableDataTypesShadow.TestNullableByte ---> [nullable tinyint] [Precision = 3 Scale = 0] BuiltInNullableDataTypesShadow.TestNullableCharacter ---> [nullable varchar] [MaxLength = 1] +BuiltInNullableDataTypesShadow.TestNullableDateOnly ---> [nullable date] BuiltInNullableDataTypesShadow.TestNullableDateTime ---> [nullable datetime] [Precision = 6] BuiltInNullableDataTypesShadow.TestNullableDateTimeOffset ---> [nullable datetime] [Precision = 6] BuiltInNullableDataTypesShadow.TestNullableDecimal ---> [nullable decimal] [Precision = 65 Scale = 30] @@ -1099,6 +1106,7 @@ public virtual void Columns_have_expected_data_types() BuiltInNullableDataTypesShadow.TestNullableInt64 ---> [nullable bigint] [Precision = 19 Scale = 0] BuiltInNullableDataTypesShadow.TestNullableSignedByte ---> [nullable tinyint] [Precision = 3 Scale = 0] BuiltInNullableDataTypesShadow.TestNullableSingle ---> [nullable float] [Precision = 12] +BuiltInNullableDataTypesShadow.TestNullableTimeOnly ---> [nullable time] [Precision = 6] BuiltInNullableDataTypesShadow.TestNullableTimeSpan ---> [nullable time] BuiltInNullableDataTypesShadow.TestNullableUnsignedInt16 ---> [nullable smallint] [Precision = 5 Scale = 0] BuiltInNullableDataTypesShadow.TestNullableUnsignedInt32 ---> [nullable int] [Precision = 10 Scale = 0] @@ -1192,9 +1200,11 @@ public virtual void Columns_have_expected_data_types() MaxLengthDataTypes.Id ---> [int] [Precision = 10 Scale = 0] MaxLengthDataTypes.String3 ---> [nullable varchar] [MaxLength = 3] MaxLengthDataTypes.String9000 ---> [nullable varchar] [MaxLength = 9000] +MaxLengthDataTypes.StringUnbounded ---> [nullable longtext] [MaxLength = -1] NonNullableBackedDataTypes.Boolean ---> [nullable tinyint] [Precision = 3 Scale = 0] NonNullableBackedDataTypes.Byte ---> [nullable tinyint] [Precision = 3 Scale = 0] NonNullableBackedDataTypes.Character ---> [nullable varchar] [MaxLength = 1] +NonNullableBackedDataTypes.DateOnly ---> [nullable date] NonNullableBackedDataTypes.DateTime ---> [nullable datetime] [Precision = 6] NonNullableBackedDataTypes.DateTimeOffset ---> [nullable datetime] [Precision = 6] NonNullableBackedDataTypes.Decimal ---> [nullable decimal] [Precision = 65 Scale = 30] @@ -1214,6 +1224,7 @@ public virtual void Columns_have_expected_data_types() NonNullableBackedDataTypes.PartitionId ---> [int] [Precision = 10 Scale = 0] NonNullableBackedDataTypes.SignedByte ---> [nullable tinyint] [Precision = 3 Scale = 0] NonNullableBackedDataTypes.Single ---> [nullable float] [Precision = 12] +NonNullableBackedDataTypes.TimeOnly ---> [nullable time] [Precision = 6] NonNullableBackedDataTypes.TimeSpan ---> [nullable time] NonNullableBackedDataTypes.UnsignedInt16 ---> [nullable smallint] [Precision = 5 Scale = 0] NonNullableBackedDataTypes.UnsignedInt32 ---> [nullable int] [Precision = 10 Scale = 0] @@ -1221,6 +1232,7 @@ public virtual void Columns_have_expected_data_types() NullableBackedDataTypes.Boolean ---> [tinyint] [Precision = 3 Scale = 0] NullableBackedDataTypes.Byte ---> [tinyint] [Precision = 3 Scale = 0] NullableBackedDataTypes.Character ---> [varchar] [MaxLength = 1] +NullableBackedDataTypes.DateOnly ---> [date] NullableBackedDataTypes.DateTime ---> [datetime] [Precision = 6] NullableBackedDataTypes.DateTimeOffset ---> [datetime] [Precision = 6] NullableBackedDataTypes.Decimal ---> [decimal] [Precision = 65 Scale = 30] @@ -1240,6 +1252,7 @@ public virtual void Columns_have_expected_data_types() NullableBackedDataTypes.PartitionId ---> [int] [Precision = 10 Scale = 0] NullableBackedDataTypes.SignedByte ---> [tinyint] [Precision = 3 Scale = 0] NullableBackedDataTypes.Single ---> [float] [Precision = 12] +NullableBackedDataTypes.TimeOnly ---> [time] [Precision = 6] NullableBackedDataTypes.TimeSpan ---> [time] NullableBackedDataTypes.UnsignedInt16 ---> [smallint] [Precision = 5 Scale = 0] NullableBackedDataTypes.UnsignedInt32 ---> [int] [Precision = 10 Scale = 0] @@ -1248,6 +1261,7 @@ public virtual void Columns_have_expected_data_types() ObjectBackedDataTypes.Byte ---> [tinyint] [Precision = 3 Scale = 0] ObjectBackedDataTypes.Bytes ---> [nullable longblob] [MaxLength = -1] ObjectBackedDataTypes.Character ---> [varchar] [MaxLength = 1] +ObjectBackedDataTypes.DateOnly ---> [date] ObjectBackedDataTypes.DateTime ---> [datetime] [Precision = 6] ObjectBackedDataTypes.DateTimeOffset ---> [datetime] [Precision = 6] ObjectBackedDataTypes.Decimal ---> [decimal] [Precision = 65 Scale = 30] @@ -1268,6 +1282,7 @@ public virtual void Columns_have_expected_data_types() ObjectBackedDataTypes.SignedByte ---> [tinyint] [Precision = 3 Scale = 0] ObjectBackedDataTypes.Single ---> [float] [Precision = 12] ObjectBackedDataTypes.String ---> [nullable longtext] [MaxLength = 4294967295] +ObjectBackedDataTypes.TimeOnly ---> [time] [Precision = 6] ObjectBackedDataTypes.TimeSpan ---> [time] ObjectBackedDataTypes.UnsignedInt16 ---> [smallint] [Precision = 5 Scale = 0] ObjectBackedDataTypes.UnsignedInt32 ---> [int] [Precision = 10 Scale = 0] @@ -1496,31 +1511,31 @@ public override void Can_insert_and_read_back_all_non_nullable_data_types() } } - [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore.")] + [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore Distributed.")] public override void Can_insert_and_read_back_with_binary_key() { base.Can_insert_and_read_back_with_binary_key(); } - [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore.")] + [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore Distributed.")] public override void Can_insert_and_read_back_with_null_binary_foreign_key() { base.Can_insert_and_read_back_with_null_binary_foreign_key(); } - [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore.")] + [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore Distributed.")] public override void Can_insert_and_read_back_with_null_string_foreign_key() { base.Can_insert_and_read_back_with_null_string_foreign_key(); } - [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore.")] + [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore Distributed.")] public override void Can_insert_and_read_back_with_string_key() { base.Can_insert_and_read_back_with_string_key(); } - [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore.")] + [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore Distributed.")] public override void Can_read_back_mapped_enum_from_collection_first_or_default() { base.Can_read_back_mapped_enum_from_collection_first_or_default(); @@ -1881,6 +1896,16 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con }); MakeRequired(modelBuilder); + + // MySQL supports a max. row size of 65535 bytes. + modelBuilder.Entity( + b => + { + // Reset the max. length of `StringUnbounded` back to -1. + // Probably a bug in the Fluent API of the class, that it is first set to -1 and immediately afterwards to 9000. + b.Property(e => e.StringUnbounded).HasMaxLength(-1); + }); + modelBuilder.Ignore(); modelBuilder.Ignore(); } diff --git a/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/ComplexTypeBulkUpdatesSingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/ComplexTypeBulkUpdatesSingleStoreTest.cs new file mode 100644 index 000000000..6f2e0d782 --- /dev/null +++ b/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/ComplexTypeBulkUpdatesSingleStoreTest.cs @@ -0,0 +1,131 @@ +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.BulkUpdates; +using Microsoft.EntityFrameworkCore.TestUtilities; +using EntityFrameworkCore.SingleStore.FunctionalTests.TestUtilities; +using Xunit; +using Xunit.Abstractions; + +namespace EntityFrameworkCore.SingleStore.FunctionalTests.BulkUpdates; + +public class ComplexTypeBulkUpdatesSingleStoreTest : ComplexTypeBulkUpdatesTestBase< + ComplexTypeBulkUpdatesSingleStoreTest.ComplexTypeBulkUpdatesSingleStoreFixture> +{ + public ComplexTypeBulkUpdatesSingleStoreTest(ComplexTypeBulkUpdatesSingleStoreFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture, testOutputHelper) + { + } + + public override async Task Delete_entity_type_with_complex_type(bool async) + { + await base.Delete_entity_type_with_complex_type(async); + + AssertSql( +""" +DELETE `c` +FROM `Customer` AS `c` +WHERE `c`.`Name` = 'Monty Elias' +"""); + } + + public override async Task Delete_complex_type_throws(bool async) + { + await base.Delete_complex_type_throws(async); + + AssertSql(); + } + + public override async Task Update_property_inside_complex_type(bool async) + { + await base.Update_property_inside_complex_type(async); + + AssertExecuteUpdateSql( +""" +UPDATE `Customer` AS `c` +SET `c`.`ShippingAddress_ZipCode` = 12345 +WHERE `c`.`ShippingAddress_ZipCode` = 7728 +"""); + } + + public override async Task Update_property_inside_nested_complex_type(bool async) + { + await base.Update_property_inside_nested_complex_type(async); + + AssertExecuteUpdateSql( +""" +UPDATE `Customer` AS `c` +SET `c`.`ShippingAddress_Country_FullName` = 'United States Modified' +WHERE `c`.`ShippingAddress_Country_Code` = 'US' +"""); + } + + public override async Task Update_multiple_properties_inside_multiple_complex_types_and_on_entity_type(bool async) + { + await base.Update_multiple_properties_inside_multiple_complex_types_and_on_entity_type(async); + + AssertExecuteUpdateSql( +""" +UPDATE `Customer` AS `c` +SET `c`.`BillingAddress_ZipCode` = 54321, + `c`.`ShippingAddress_ZipCode` = `c`.`BillingAddress_ZipCode`, + `c`.`Name` = CONCAT(`c`.`Name`, 'Modified') +WHERE `c`.`ShippingAddress_ZipCode` = 7728 +"""); + } + + public override async Task Update_projected_complex_type(bool async) + { + await base.Update_projected_complex_type(async); + + AssertExecuteUpdateSql( +""" +UPDATE `Customer` AS `c` +SET `c`.`ShippingAddress_ZipCode` = 12345 +"""); + } + + public override async Task Update_multiple_projected_complex_types_via_anonymous_type(bool async) + { + await base.Update_multiple_projected_complex_types_via_anonymous_type(async); + + AssertExecuteUpdateSql( +""" +UPDATE `Customer` AS `c` +SET `c`.`BillingAddress_ZipCode` = 54321, + `c`.`ShippingAddress_ZipCode` = `c`.`BillingAddress_ZipCode` +"""); + } + + public override async Task Update_projected_complex_type_via_OrderBy_Skip_throws(bool async) + { + await base.Update_projected_complex_type_via_OrderBy_Skip_throws(async); + + AssertExecuteUpdateSql(); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + { + TestHelpers.AssertAllMethodsOverridden(GetType()); + } + + private void AssertExecuteUpdateSql(params string[] expected) + { + Fixture.TestSqlLoggerFactory.AssertBaseline(expected, forUpdate: true); + } + + private void AssertSql(params string[] expected) + { + Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + } + + protected void ClearLog() + { + Fixture.TestSqlLoggerFactory.Clear(); + } + + public class ComplexTypeBulkUpdatesSingleStoreFixture : ComplexTypeBulkUpdatesFixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => SingleStoreTestStoreFactory.Instance; + } +} diff --git a/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesSingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesSingleStoreTest.cs index a66f196bc..214d7a222 100644 --- a/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesSingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesSingleStoreTest.cs @@ -1,7 +1,9 @@ +using System; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.BulkUpdates; using Microsoft.EntityFrameworkCore.TestUtilities; using EntityFrameworkCore.SingleStore.FunctionalTests.TestUtilities; +using Microsoft.EntityFrameworkCore; using Xunit; namespace EntityFrameworkCore.SingleStore.FunctionalTests.BulkUpdates; @@ -40,19 +42,152 @@ public override async Task Delete_aggregate_root_when_table_sharing_with_non_own AssertSql(); } - public override async Task Delete_predicate_based_on_optional_navigation(bool async) + public override async Task Delete_entity_with_auto_include(bool async) { - await base.Delete_predicate_based_on_optional_navigation(async); + await base.Delete_entity_with_auto_include(async); AssertSql( """ - DELETE `p` - FROM `Posts` AS `p` - LEFT JOIN `Blogs` AS `b` ON `p`.`BlogId` = `b`.`Id` - WHERE `b`.`Title` IS NOT NULL AND (`b`.`Title` LIKE 'Arthur%') + DELETE `c` + FROM `Context30572_Principal` AS `c` + LEFT JOIN `Context30572_Dependent` AS `c0` ON `c`.`DependentId` = `c0`.`Id` """); } + public override async Task Delete_predicate_based_on_optional_navigation(bool async) + { + await base.Delete_predicate_based_on_optional_navigation(async); + + AssertSql( +""" +DELETE `p` +FROM `Posts` AS `p` +LEFT JOIN `Blogs` AS `b` ON `p`.`BlogId` = `b`.`Id` +WHERE `b`.`Title` LIKE 'Arthur%' +"""); + } + + public override async Task Update_non_owned_property_on_entity_with_owned(bool async) + { + await base.Update_non_owned_property_on_entity_with_owned(async); + + AssertSql( +""" +UPDATE `Owner` AS `o` +SET `o`.`Title` = 'SomeValue' +"""); + } + + public override async Task Update_non_owned_property_on_entity_with_owned2(bool async) + { + await base.Update_non_owned_property_on_entity_with_owned2(async); + + AssertSql( +""" +UPDATE `Owner` AS `o` +SET `o`.`Title` = CONCAT(COALESCE(`o`.`Title`, ''), '_Suffix') +"""); + } + + public override async Task Update_owned_and_non_owned_properties_with_table_sharing(bool async) + { + await base.Update_owned_and_non_owned_properties_with_table_sharing(async); + + AssertSql( +""" +UPDATE `Owner` AS `o` +SET `o`.`OwnedReference_Number` = CHAR_LENGTH(`o`.`Title`), + `o`.`Title` = CAST(`o`.`OwnedReference_Number` AS char) +"""); + } + + public override async Task Update_main_table_in_entity_with_entity_splitting(bool async) + { + var contextFactory = await InitializeAsync( + onModelCreating: mb => + { + mb.Entity(b => + { + // Set column type + b.Property(p => p.Id).HasColumnType("bigint"); + + // Split entity across two tables + b.ToTable("Blogs") + .SplitToTable("BlogsPart1", tb => + { + tb.Property(p => p.Title); + tb.Property(p => p.Rating); + }); + }); + + mb.Entity(b => + b.Property(p => p.Id).HasColumnType("bigint")); + }, + seed: context => + { + context.Set().Add(new Blog { Title = "SomeBlog" }); + context.SaveChanges(); + }); + + await AssertUpdate( + async, + contextFactory.CreateContext, + ss => ss.Set(), + s => s.SetProperty(b => b.CreationTimestamp, b => new DateTime(2020, 1, 1)), + rowsAffectedCount: 1); + + AssertSql( +""" +UPDATE `Blogs` AS `b` +SET `b`.`CreationTimestamp` = '2020-01-01 00:00:00' +"""); + } + + public override async Task Update_non_main_table_in_entity_with_entity_splitting(bool async) + { + var contextFactory = await InitializeAsync( + onModelCreating: mb => + { + mb.Entity(b => + { + // Set column type + b.Property(p => p.Id).HasColumnType("bigint"); + + // Split entity across two tables + b.ToTable("Blogs") + .SplitToTable("BlogsPart1", tb => + { + tb.Property(p => p.Title); + tb.Property(p => p.Rating); + }); + }); + + mb.Entity(b => + b.Property(p => p.Id).HasColumnType("bigint")); + }, + seed: context => + { + context.Set().Add(new Blog { Title = "SomeBlog" }); + context.SaveChanges(); + }); + + await AssertUpdate( + async, + contextFactory.CreateContext, + ss => ss.Set(), + s => s + .SetProperty(b => b.Title, b => b.Rating.ToString()) + .SetProperty(b => b.Rating, b => b.Title!.Length), + rowsAffectedCount: 1); + + AssertSql( +""" +UPDATE `BlogsPart1` AS `b0` +SET `b0`.`Rating` = CHAR_LENGTH(`b0`.`Title`), + `b0`.`Title` = CAST(`b0`.`Rating` AS char) +"""); + } + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore.")] public override async Task Update_with_alias_uniquification_in_setter_subquery(bool async) { diff --git a/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSingleStoreFixture.cs b/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSingleStoreFixture.cs index cf585adb5..db19e172a 100644 --- a/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSingleStoreFixture.cs +++ b/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSingleStoreFixture.cs @@ -1,6 +1,8 @@ +using System; using Microsoft.EntityFrameworkCore.BulkUpdates; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.TestUtilities; +using EntityFrameworkCore.SingleStore.FunctionalTests.TestModels.Northwind; using EntityFrameworkCore.SingleStore.FunctionalTests.TestUtilities; namespace EntityFrameworkCore.SingleStore.FunctionalTests.BulkUpdates; @@ -10,4 +12,7 @@ public class NorthwindBulkUpdatesSingleStoreFixture : Northwin { protected override ITestStoreFactory TestStoreFactory => SingleStoreNorthwindTestStoreFactory.Instance; + + protected override Type ContextType + => typeof(NorthwindSingleStoreContext); } diff --git a/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSingleStoreTest.cs index 76e63aa97..e4e652e5a 100644 --- a/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSingleStoreTest.cs @@ -16,7 +16,7 @@ public class NorthwindBulkUpdatesSingleStoreTest : NorthwindBulkUpdatesTestBase< public NorthwindBulkUpdatesSingleStoreTest( NorthwindBulkUpdatesSingleStoreFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) + : base(fixture, testOutputHelper) { ClearLog(); // Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); @@ -363,7 +363,7 @@ public override async Task Delete_Where_using_navigation_2(bool async) FROM `Order Details` AS `o` INNER JOIN `Orders` AS `o0` ON `o`.`OrderID` = `o0`.`OrderID` LEFT JOIN `Customers` AS `c` ON `o0`.`CustomerID` = `c`.`CustomerID` -WHERE `c`.`CustomerID` IS NOT NULL AND (`c`.`CustomerID` LIKE 'F%') +WHERE `c`.`CustomerID` LIKE 'F%' """); } @@ -509,7 +509,7 @@ public override async Task Delete_Where_optional_navigation_predicate(bool async FROM `Order Details` AS `o` INNER JOIN `Orders` AS `o0` ON `o`.`OrderID` = `o0`.`OrderID` LEFT JOIN `Customers` AS `c` ON `o0`.`CustomerID` = `c`.`CustomerID` -WHERE `c`.`City` IS NOT NULL AND (`c`.`City` LIKE 'Se%') +WHERE `c`.`City` LIKE 'Se%' """); } @@ -644,8 +644,8 @@ public override async Task Update_Where_parameter_set_constant(bool async) await base.Update_Where_parameter_set_constant(async); AssertExecuteUpdateSql( - """ -@__customer_0='ALFKI' (Size = 255) +""" +@__customer_0='ALFKI' (Size = 5) (DbType = StringFixedLength) UPDATE `Customers` AS `c` SET `c`.`ContactName` = 'Updated' @@ -653,7 +653,7 @@ public override async Task Update_Where_parameter_set_constant(bool async) """, // """ -@__customer_0='ALFKI' (Size = 255) +@__customer_0='ALFKI' (Size = 5) (DbType = StringFixedLength) SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` @@ -679,7 +679,7 @@ public override async Task Update_Where_set_parameter(bool async) AssertExecuteUpdateSql( """ -@__value_0='Abc' (Size = 4000) +@__value_0='Abc' (Size = 30) UPDATE `Customers` AS `c` SET `c`.`ContactName` = @__value_0 @@ -693,7 +693,7 @@ public override async Task Update_Where_set_parameter_from_closure_array(bool as AssertExecuteUpdateSql( """ -@__p_0='Abc' (Size = 4000) +@__p_0='Abc' (Size = 30) UPDATE `Customers` AS `c` SET `c`.`ContactName` = @__p_0 @@ -719,7 +719,7 @@ public override async Task Update_Where_set_parameter_from_multilevel_property_a AssertExecuteUpdateSql( """ -@__container_Containee_Property_0='Abc' (Size = 4000) +@__container_Containee_Property_0='Abc' (Size = 30) UPDATE `Customers` AS `c` SET `c`.`ContactName` = @__container_Containee_Property_0 @@ -974,8 +974,12 @@ public override async Task Update_Where_Distinct_set_constant(bool async) AssertExecuteUpdateSql( """ UPDATE `Customers` AS `c` +INNER JOIN ( + SELECT DISTINCT `c0`.`CustomerID`, `c0`.`Address`, `c0`.`City`, `c0`.`CompanyName`, `c0`.`ContactName`, `c0`.`ContactTitle`, `c0`.`Country`, `c0`.`Fax`, `c0`.`Phone`, `c0`.`PostalCode`, `c0`.`Region` + FROM `Customers` AS `c0` + WHERE `c0`.`CustomerID` LIKE 'F%' +) AS `t` ON `c`.`CustomerID` = `t`.`CustomerID` SET `c`.`ContactName` = 'Updated' -WHERE `c`.`CustomerID` LIKE 'F%' """); } @@ -1038,7 +1042,7 @@ public override async Task Update_Where_set_property_plus_parameter(bool async) AssertExecuteUpdateSql( """ -@__value_0='Abc' (Size = 4000) +@__value_0='Abc' (Size = 30) UPDATE `Customers` AS `c` SET `c`.`ContactName` = CONCAT(COALESCE(`c`.`ContactName`, ''), @__value_0) @@ -1102,7 +1106,7 @@ public override async Task Update_Where_multiple_set(bool async) AssertExecuteUpdateSql( """ -@__value_0='Abc' (Size = 4000) +@__value_0='Abc' (Size = 30) UPDATE `Customers` AS `c` SET `c`.`City` = 'Seattle', @@ -1118,13 +1122,6 @@ public override async Task Update_with_invalid_lambda_in_set_property_throws(boo AssertExecuteUpdateSql(); } - public override async Task Update_multiple_entity_throws(bool async) - { - await base.Update_multiple_entity_throws(async); - - AssertExecuteUpdateSql(); - } - public override async Task Update_unmapped_property_throws(bool async) { await base.Update_unmapped_property_throws(async); @@ -1307,7 +1304,7 @@ public override async Task Update_with_cross_join_left_join_set_constant(bool as CROSS JOIN ( SELECT `c0`.`CustomerID`, `c0`.`Address`, `c0`.`City`, `c0`.`CompanyName`, `c0`.`ContactName`, `c0`.`ContactTitle`, `c0`.`Country`, `c0`.`Fax`, `c0`.`Phone`, `c0`.`PostalCode`, `c0`.`Region` FROM `Customers` AS `c0` - WHERE `c0`.`City` IS NOT NULL AND (`c0`.`City` LIKE 'S%') + WHERE `c0`.`City` LIKE 'S%' ) AS `t` LEFT JOIN ( SELECT `o`.`OrderID`, `o`.`CustomerID`, `o`.`EmployeeID`, `o`.`OrderDate` @@ -1329,7 +1326,7 @@ public override async Task Update_with_cross_join_cross_apply_set_constant(bool CROSS JOIN ( SELECT `c0`.`CustomerID`, `c0`.`Address`, `c0`.`City`, `c0`.`CompanyName`, `c0`.`ContactName`, `c0`.`ContactTitle`, `c0`.`Country`, `c0`.`Fax`, `c0`.`Phone`, `c0`.`PostalCode`, `c0`.`Region` FROM `Customers` AS `c0` - WHERE `c0`.`City` IS NOT NULL AND (`c0`.`City` LIKE 'S%') + WHERE `c0`.`City` LIKE 'S%' ) AS `t` JOIN LATERAL ( SELECT `o`.`OrderID`, `o`.`CustomerID`, `o`.`EmployeeID`, `o`.`OrderDate` @@ -1351,7 +1348,7 @@ public override async Task Update_with_cross_join_outer_apply_set_constant(bool CROSS JOIN ( SELECT `c0`.`CustomerID`, `c0`.`Address`, `c0`.`City`, `c0`.`CompanyName`, `c0`.`ContactName`, `c0`.`ContactTitle`, `c0`.`Country`, `c0`.`Fax`, `c0`.`Phone`, `c0`.`PostalCode`, `c0`.`Region` FROM `Customers` AS `c0` - WHERE `c0`.`City` IS NOT NULL AND (`c0`.`City` LIKE 'S%') + WHERE `c0`.`City` LIKE 'S%' ) AS `t` LEFT JOIN LATERAL ( SELECT `o`.`OrderID`, `o`.`CustomerID`, `o`.`EmployeeID`, `o`.`OrderDate` @@ -1444,6 +1441,49 @@ ORDER BY `o`.`OrderDate` DESC """); } + public override async Task Update_multiple_tables_throws(bool async) + { + await base.Update_multiple_tables_throws(async); + + AssertSql( + """ + SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` + FROM `Orders` AS `o` + LEFT JOIN `Customers` AS `c` ON `o`.`CustomerID` = `c`.`CustomerID` + WHERE `o`.`CustomerID` LIKE 'F%' + """); + } + + public override async Task Update_with_two_inner_joins(bool async) + { + await base.Update_with_two_inner_joins(async); + + AssertSql( + """ + SELECT `o`.`OrderID`, `o`.`ProductID`, `o`.`Discount`, `o`.`Quantity`, `o`.`UnitPrice` + FROM `Order Details` AS `o` + INNER JOIN `Products` AS `p` ON `o`.`ProductID` = `p`.`ProductID` + INNER JOIN `Orders` AS `o0` ON `o`.`OrderID` = `o0`.`OrderID` + WHERE `p`.`Discontinued` AND (`o0`.`OrderDate` > '1990-01-01 00:00:00') + """, + // + """ + UPDATE `Order Details` AS `o` + INNER JOIN `Products` AS `p` ON `o`.`ProductID` = `p`.`ProductID` + INNER JOIN `Orders` AS `o0` ON `o`.`OrderID` = `o0`.`OrderID` + SET `o`.`Quantity` = CAST(1 AS signed) + WHERE `p`.`Discontinued` AND (`o0`.`OrderDate` > '1990-01-01 00:00:00') + """, + // + """ + SELECT `o`.`OrderID`, `o`.`ProductID`, `o`.`Discount`, `o`.`Quantity`, `o`.`UnitPrice` + FROM `Order Details` AS `o` + INNER JOIN `Products` AS `p` ON `o`.`ProductID` = `p`.`ProductID` + INNER JOIN `Orders` AS `o0` ON `o`.`OrderID` = `o0`.`OrderID` + WHERE `p`.`Discontinued` AND (`o0`.`OrderDate` > '1990-01-01 00:00:00') + """); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesSingleStoreFixture.cs b/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesSingleStoreFixture.cs index a03b900ae..2a47a0e8b 100644 --- a/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesSingleStoreFixture.cs +++ b/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesSingleStoreFixture.cs @@ -5,6 +5,6 @@ public class TPCFiltersInheritanceBulkUpdatesSingleStoreFixture : TPCInheritance protected override string StoreName => "TPCFiltersInheritanceBulkUpdatesTest"; - protected override bool EnableFilters + public override bool EnableFilters => true; } diff --git a/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesSingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesSingleStoreTest.cs index 7e6f63d98..bfbb8980b 100644 --- a/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesSingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesSingleStoreTest.cs @@ -2,14 +2,17 @@ using Microsoft.EntityFrameworkCore.BulkUpdates; using Microsoft.EntityFrameworkCore.TestUtilities; using Xunit; +using Xunit.Abstractions; namespace EntityFrameworkCore.SingleStore.FunctionalTests.BulkUpdates; public class TPCFiltersInheritanceBulkUpdatesSingleStoreTest : TPCFiltersInheritanceBulkUpdatesTestBase< TPCFiltersInheritanceBulkUpdatesSingleStoreFixture> { - public TPCFiltersInheritanceBulkUpdatesSingleStoreTest(TPCFiltersInheritanceBulkUpdatesSingleStoreFixture fixture) - : base(fixture) + public TPCFiltersInheritanceBulkUpdatesSingleStoreTest( + TPCFiltersInheritanceBulkUpdatesSingleStoreFixture fixture, + ITestOutputHelper testOutputHelper) + : base(fixture, testOutputHelper) { ClearLog(); } @@ -111,13 +114,6 @@ public override async Task Delete_GroupBy_Where_Select_First_3(bool async) AssertSql(); } - public override async Task Update_where_hierarchy(bool async) - { - await base.Update_where_hierarchy(async); - - AssertExecuteUpdateSql(); - } - public override async Task Update_where_hierarchy_subquery(bool async) { await base.Update_where_hierarchy_subquery(async); @@ -125,18 +121,6 @@ public override async Task Update_where_hierarchy_subquery(bool async) AssertExecuteUpdateSql(); } - public override async Task Update_where_hierarchy_derived(bool async) - { - await base.Update_where_hierarchy_derived(async); - - AssertExecuteUpdateSql( -""" -UPDATE `Kiwi` AS `k` -SET `k`.`Name` = 'Kiwi' -WHERE (`k`.`CountryId` = 1) AND (`k`.`Name` = 'Great spotted kiwi') -"""); - } - public override async Task Update_where_using_hierarchy(bool async) { await base.Update_where_using_hierarchy(async); @@ -183,6 +167,112 @@ public override async Task Update_where_keyless_entity_mapped_to_sql_query(bool AssertExecuteUpdateSql(); } + public override async Task Update_base_type(bool async) + { + await base.Update_base_type(async); + + AssertSql( +""" +SELECT `t`.`Id`, `t`.`CountryId`, `t`.`Name`, `t`.`Species`, `t`.`EagleId`, `t`.`IsFlightless`, `t`.`Group`, `t`.`FoundOn`, `t`.`Discriminator` +FROM ( + SELECT `e`.`Id`, `e`.`CountryId`, `e`.`Name`, `e`.`Species`, `e`.`EagleId`, `e`.`IsFlightless`, `e`.`Group`, NULL AS `FoundOn`, 'Eagle' AS `Discriminator` + FROM `Eagle` AS `e` + UNION ALL + SELECT `k`.`Id`, `k`.`CountryId`, `k`.`Name`, `k`.`Species`, `k`.`EagleId`, `k`.`IsFlightless`, NULL AS `Group`, `k`.`FoundOn`, 'Kiwi' AS `Discriminator` + FROM `Kiwi` AS `k` +) AS `t` +WHERE (`t`.`CountryId` = 1) AND (`t`.`Name` = 'Great spotted kiwi') +"""); + } + + public override async Task Update_base_type_with_OfType(bool async) + { + await base.Update_base_type_with_OfType(async); + + AssertSql( +""" +SELECT `t`.`Id`, `t`.`CountryId`, `t`.`Name`, `t`.`Species`, `t`.`EagleId`, `t`.`IsFlightless`, `t`.`FoundOn`, `t`.`Discriminator` +FROM ( + SELECT `k`.`Id`, `k`.`CountryId`, `k`.`Name`, `k`.`Species`, `k`.`EagleId`, `k`.`IsFlightless`, `k`.`FoundOn`, 'Kiwi' AS `Discriminator` + FROM `Kiwi` AS `k` +) AS `t` +WHERE `t`.`CountryId` = 1 +"""); + } + + public override async Task Update_base_property_on_derived_type(bool async) + { + await base.Update_base_property_on_derived_type(async); + + AssertSql( +""" +SELECT `k`.`Id`, `k`.`CountryId`, `k`.`Name`, `k`.`Species`, `k`.`EagleId`, `k`.`IsFlightless`, `k`.`FoundOn` +FROM `Kiwi` AS `k` +WHERE `k`.`CountryId` = 1 +""", + // + """ +UPDATE `Kiwi` AS `k` +SET `k`.`Name` = 'SomeOtherKiwi' +WHERE `k`.`CountryId` = 1 +""", + // + """ +SELECT `k`.`Id`, `k`.`CountryId`, `k`.`Name`, `k`.`Species`, `k`.`EagleId`, `k`.`IsFlightless`, `k`.`FoundOn` +FROM `Kiwi` AS `k` +WHERE `k`.`CountryId` = 1 +"""); + } + + public override async Task Update_derived_property_on_derived_type(bool async) + { + await base.Update_derived_property_on_derived_type(async); + + AssertSql( +""" +SELECT `k`.`Id`, `k`.`CountryId`, `k`.`Name`, `k`.`Species`, `k`.`EagleId`, `k`.`IsFlightless`, `k`.`FoundOn` +FROM `Kiwi` AS `k` +WHERE `k`.`CountryId` = 1 +""", + // + """ +UPDATE `Kiwi` AS `k` +SET `k`.`FoundOn` = 0 +WHERE `k`.`CountryId` = 1 +""", + // + """ +SELECT `k`.`Id`, `k`.`CountryId`, `k`.`Name`, `k`.`Species`, `k`.`EagleId`, `k`.`IsFlightless`, `k`.`FoundOn` +FROM `Kiwi` AS `k` +WHERE `k`.`CountryId` = 1 +"""); + } + + public override async Task Update_base_and_derived_types(bool async) + { + await base.Update_base_and_derived_types(async); + + AssertSql( +""" +SELECT `k`.`Id`, `k`.`CountryId`, `k`.`Name`, `k`.`Species`, `k`.`EagleId`, `k`.`IsFlightless`, `k`.`FoundOn` +FROM `Kiwi` AS `k` +WHERE `k`.`CountryId` = 1 +""", + // + """ +UPDATE `Kiwi` AS `k` +SET `k`.`FoundOn` = 0, + `k`.`Name` = 'Kiwi' +WHERE `k`.`CountryId` = 1 +""", + // + """ +SELECT `k`.`Id`, `k`.`CountryId`, `k`.`Name`, `k`.`Species`, `k`.`EagleId`, `k`.`IsFlightless`, `k`.`FoundOn` +FROM `Kiwi` AS `k` +WHERE `k`.`CountryId` = 1 +"""); + } + protected override void ClearLog() => Fixture.TestSqlLoggerFactory.Clear(); diff --git a/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesSingleStoreFixture.cs b/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesSingleStoreFixture.cs index 85caaf12a..6e9f9012c 100644 --- a/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesSingleStoreFixture.cs +++ b/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesSingleStoreFixture.cs @@ -12,7 +12,7 @@ public class TPCInheritanceBulkUpdatesSingleStoreFixture : TPCInheritanceBulkUpd protected override ITestStoreFactory TestStoreFactory => SingleStoreTestStoreFactory.Instance; - protected override bool UseGeneratedKeys + public override bool UseGeneratedKeys => false; protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) diff --git a/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesSingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesSingleStoreTest.cs index d6b1e7631..dc28a5585 100644 --- a/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesSingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesSingleStoreTest.cs @@ -2,13 +2,16 @@ using Microsoft.EntityFrameworkCore.BulkUpdates; using Microsoft.EntityFrameworkCore.TestUtilities; using Xunit; +using Xunit.Abstractions; namespace EntityFrameworkCore.SingleStore.FunctionalTests.BulkUpdates; public class TPCInheritanceBulkUpdatesSingleStoreTest : TPCInheritanceBulkUpdatesTestBase { - public TPCInheritanceBulkUpdatesSingleStoreTest(TPCInheritanceBulkUpdatesSingleStoreFixture fixture) - : base(fixture) + public TPCInheritanceBulkUpdatesSingleStoreTest( + TPCInheritanceBulkUpdatesSingleStoreFixture fixture, + ITestOutputHelper testOutputHelper) + : base(fixture, testOutputHelper) { ClearLog(); } @@ -106,13 +109,6 @@ public override async Task Delete_GroupBy_Where_Select_First_3(bool async) AssertSql(); } - public override async Task Update_where_hierarchy(bool async) - { - await base.Update_where_hierarchy(async); - - AssertExecuteUpdateSql(); - } - public override async Task Update_where_hierarchy_subquery(bool async) { await base.Update_where_hierarchy_subquery(async); @@ -120,18 +116,6 @@ public override async Task Update_where_hierarchy_subquery(bool async) AssertExecuteUpdateSql(); } - public override async Task Update_where_hierarchy_derived(bool async) - { - await base.Update_where_hierarchy_derived(async); - - AssertExecuteUpdateSql( -""" -UPDATE `Kiwi` AS `k` -SET `k`.`Name` = 'Kiwi' -WHERE `k`.`Name` = 'Great spotted kiwi' -"""); - } - public override async Task Update_where_using_hierarchy(bool async) { await base.Update_where_using_hierarchy(async); @@ -178,6 +162,141 @@ public override async Task Update_where_keyless_entity_mapped_to_sql_query(bool AssertExecuteUpdateSql(); } + public override async Task Update_base_type(bool async) + { + await base.Update_base_type(async); + + AssertSql( +""" +SELECT `t`.`Id`, `t`.`CountryId`, `t`.`Name`, `t`.`Species`, `t`.`EagleId`, `t`.`IsFlightless`, `t`.`Group`, `t`.`FoundOn`, `t`.`Discriminator` +FROM ( + SELECT `e`.`Id`, `e`.`CountryId`, `e`.`Name`, `e`.`Species`, `e`.`EagleId`, `e`.`IsFlightless`, `e`.`Group`, NULL AS `FoundOn`, 'Eagle' AS `Discriminator` + FROM `Eagle` AS `e` + UNION ALL + SELECT `k`.`Id`, `k`.`CountryId`, `k`.`Name`, `k`.`Species`, `k`.`EagleId`, `k`.`IsFlightless`, NULL AS `Group`, `k`.`FoundOn`, 'Kiwi' AS `Discriminator` + FROM `Kiwi` AS `k` +) AS `t` +WHERE `t`.`Name` = 'Great spotted kiwi' +"""); + } + + public override async Task Update_base_type_with_OfType(bool async) + { + await base.Update_base_type_with_OfType(async); + + AssertSql( +""" +SELECT `k`.`Id`, `k`.`CountryId`, `k`.`Name`, `k`.`Species`, `k`.`EagleId`, `k`.`IsFlightless`, `k`.`FoundOn`, 'Kiwi' AS `Discriminator` +FROM `Kiwi` AS `k` +"""); + } + + public override async Task Update_base_property_on_derived_type(bool async) + { + await base.Update_base_property_on_derived_type(async); + + AssertSql( +""" +SELECT `k`.`Id`, `k`.`CountryId`, `k`.`Name`, `k`.`Species`, `k`.`EagleId`, `k`.`IsFlightless`, `k`.`FoundOn` +FROM `Kiwi` AS `k` +""", + // + """ +UPDATE `Kiwi` AS `k` +SET `k`.`Name` = 'SomeOtherKiwi' +""", + // + """ +SELECT `k`.`Id`, `k`.`CountryId`, `k`.`Name`, `k`.`Species`, `k`.`EagleId`, `k`.`IsFlightless`, `k`.`FoundOn` +FROM `Kiwi` AS `k` +"""); + } + + public override async Task Update_derived_property_on_derived_type(bool async) + { + await base.Update_derived_property_on_derived_type(async); + + AssertSql( +""" +SELECT `k`.`Id`, `k`.`CountryId`, `k`.`Name`, `k`.`Species`, `k`.`EagleId`, `k`.`IsFlightless`, `k`.`FoundOn` +FROM `Kiwi` AS `k` +""", + // + """ +UPDATE `Kiwi` AS `k` +SET `k`.`FoundOn` = 0 +""", + // + """ +SELECT `k`.`Id`, `k`.`CountryId`, `k`.`Name`, `k`.`Species`, `k`.`EagleId`, `k`.`IsFlightless`, `k`.`FoundOn` +FROM `Kiwi` AS `k` +"""); + } + + public override async Task Update_base_and_derived_types(bool async) + { + await base.Update_base_and_derived_types(async); + + AssertSql( +""" +SELECT `k`.`Id`, `k`.`CountryId`, `k`.`Name`, `k`.`Species`, `k`.`EagleId`, `k`.`IsFlightless`, `k`.`FoundOn` +FROM `Kiwi` AS `k` +""", + // + """ +UPDATE `Kiwi` AS `k` +SET `k`.`FoundOn` = 0, + `k`.`Name` = 'Kiwi' +""", + // + """ +SELECT `k`.`Id`, `k`.`CountryId`, `k`.`Name`, `k`.`Species`, `k`.`EagleId`, `k`.`IsFlightless`, `k`.`FoundOn` +FROM `Kiwi` AS `k` +"""); + } + + public override async Task Update_with_interface_in_property_expression(bool async) + { + await base.Update_with_interface_in_property_expression(async); + + AssertSql( +""" +SELECT `c`.`Id`, `c`.`SortIndex`, `c`.`CaffeineGrams`, `c`.`CokeCO2`, `c`.`SugarGrams` +FROM `Coke` AS `c` +""", + // + """ +UPDATE `Coke` AS `c` +SET `c`.`SugarGrams` = 0 +""", + // + """ +SELECT `c`.`Id`, `c`.`SortIndex`, `c`.`CaffeineGrams`, `c`.`CokeCO2`, `c`.`SugarGrams` +FROM `Coke` AS `c` +"""); + } + + public override async Task Update_with_interface_in_EF_Property_in_property_expression(bool async) + { + await base.Update_with_interface_in_EF_Property_in_property_expression(async); + + AssertSql( +""" +SELECT `c`.`Id`, `c`.`SortIndex`, `c`.`CaffeineGrams`, `c`.`CokeCO2`, `c`.`SugarGrams` +FROM `Coke` AS `c` +""", + // + """ +UPDATE `Coke` AS `c` +SET `c`.`SugarGrams` = 0 +""", + // + """ +SELECT `c`.`Id`, `c`.`SortIndex`, `c`.`CaffeineGrams`, `c`.`CokeCO2`, `c`.`SugarGrams` +FROM `Coke` AS `c` +"""); + } + protected override void ClearLog() => Fixture.TestSqlLoggerFactory.Clear(); diff --git a/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/FiltersInheritanceBulkUpdatesSingleStoreFixture.cs b/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/TPHFiltersInheritanceBulkUpdatesSingleStoreFixture.cs similarity index 86% rename from test/EFCore.SingleStore.FunctionalTests/BulkUpdates/FiltersInheritanceBulkUpdatesSingleStoreFixture.cs rename to test/EFCore.SingleStore.FunctionalTests/BulkUpdates/TPHFiltersInheritanceBulkUpdatesSingleStoreFixture.cs index 7a7e820c0..40543edda 100644 --- a/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/FiltersInheritanceBulkUpdatesSingleStoreFixture.cs +++ b/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/TPHFiltersInheritanceBulkUpdatesSingleStoreFixture.cs @@ -3,12 +3,12 @@ namespace EntityFrameworkCore.SingleStore.FunctionalTests.BulkUpdates; -public class FiltersInheritanceBulkUpdatesSingleStoreFixture : InheritanceBulkUpdatesSingleStoreFixture +public class TPHFiltersInheritanceBulkUpdatesSingleStoreFixture : TPHInheritanceBulkUpdatesSingleStoreFixture { protected override string StoreName => "FiltersInheritanceBulkUpdatesTest"; - protected override bool EnableFilters + public override bool EnableFilters => true; protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) diff --git a/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/FiltersInheritanceBulkUpdatesSingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/TPHFiltersInheritanceBulkUpdatesSingleStoreTest.cs similarity index 64% rename from test/EFCore.SingleStore.FunctionalTests/BulkUpdates/FiltersInheritanceBulkUpdatesSingleStoreTest.cs rename to test/EFCore.SingleStore.FunctionalTests/BulkUpdates/TPHFiltersInheritanceBulkUpdatesSingleStoreTest.cs index 48f152cda..d583bf4a9 100644 --- a/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/FiltersInheritanceBulkUpdatesSingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/TPHFiltersInheritanceBulkUpdatesSingleStoreTest.cs @@ -1,15 +1,20 @@ +using System; using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.BulkUpdates; using Microsoft.EntityFrameworkCore.TestUtilities; using SingleStoreConnector; +using EntityFrameworkCore.SingleStore.Infrastructure; +using EntityFrameworkCore.SingleStore.Tests; +using EntityFrameworkCore.SingleStore.Tests.TestUtilities.Attributes; using Xunit; namespace EntityFrameworkCore.SingleStore.FunctionalTests.BulkUpdates; -public class FiltersInheritanceBulkUpdatesSingleStoreTest : FiltersInheritanceBulkUpdatesTestBase< - FiltersInheritanceBulkUpdatesSingleStoreFixture> +public class TPHFiltersInheritanceBulkUpdatesSingleStoreTest : FiltersInheritanceBulkUpdatesTestBase< + TPHFiltersInheritanceBulkUpdatesSingleStoreFixture> { - public FiltersInheritanceBulkUpdatesSingleStoreTest(FiltersInheritanceBulkUpdatesSingleStoreFixture fixture) + public TPHFiltersInheritanceBulkUpdatesSingleStoreTest(TPHFiltersInheritanceBulkUpdatesSingleStoreFixture fixture) : base(fixture) { ClearLog(); @@ -89,63 +94,73 @@ public override async Task Delete_GroupBy_Where_Select_First_2(bool async) public override async Task Delete_GroupBy_Where_Select_First_3(bool async) { - // Not supported by MySQL: - // Error Code: 1093. You can't specify target table 'c' for update in FROM clause - await Assert.ThrowsAsync( - () => base.Delete_GroupBy_Where_Select_First_3(async)); + await base.Delete_GroupBy_Where_Select_First_3(async); AssertSql( """ DELETE `a` FROM `Animals` AS `a` -WHERE (`a`.`CountryId` = 1) AND EXISTS ( - SELECT 1 - FROM `Animals` AS `a0` - WHERE `a0`.`CountryId` = 1 - GROUP BY `a0`.`CountryId` - HAVING (COUNT(*) < 3) AND (( +WHERE (`a`.`CountryId` = 1) AND `a`.`Id` IN ( + SELECT ( SELECT `a1`.`Id` FROM `Animals` AS `a1` WHERE (`a1`.`CountryId` = 1) AND (`a0`.`CountryId` = `a1`.`CountryId`) - LIMIT 1) = `a`.`Id`)) -""" -); + LIMIT 1) + FROM `Animals` AS `a0` + WHERE `a0`.`CountryId` = 1 + GROUP BY `a0`.`CountryId` + HAVING COUNT(*) < 3 +) +"""); } - public override async Task Delete_where_keyless_entity_mapped_to_sql_query(bool async) + public override async Task Update_where_hierarchy_subquery(bool async) { - await base.Delete_where_keyless_entity_mapped_to_sql_query(async); + await base.Update_where_hierarchy_subquery(async); - AssertSql(); + AssertExecuteUpdateSql(); } - public override async Task Delete_where_hierarchy_subquery(bool async) + public override async Task Update_where_using_hierarchy(bool async) { - await base.Delete_where_hierarchy_subquery(async); + await base.Update_where_using_hierarchy(async); - AssertSql( + AssertExecuteUpdateSql( """ -@__p_1='3' -@__p_0='0' - -DELETE `a` -FROM `Animals` AS `a` -WHERE EXISTS ( - SELECT 1 - FROM ( - SELECT `a0`.`Id`, `a0`.`CountryId`, `a0`.`Discriminator`, `a0`.`Name`, `a0`.`Species`, `a0`.`EagleId`, `a0`.`IsFlightless`, `a0`.`Group`, `a0`.`FoundOn` - FROM `Animals` AS `a0` - WHERE (`a0`.`CountryId` = 1) AND (`a0`.`Name` = 'Great spotted kiwi') - ORDER BY `a0`.`Name` - LIMIT @__p_1 OFFSET @__p_0 - ) AS `t` - WHERE `t`.`Id` = `a`.`Id`) +UPDATE `Countries` AS `c` +SET `c`.`Name` = 'Monovia' +WHERE ( + SELECT COUNT(*) + FROM `Animals` AS `a` + WHERE ((`a`.`CountryId` = 1) AND (`c`.`Id` = `a`.`CountryId`)) AND (`a`.`CountryId` > 0)) > 0 """); } - public override async Task Update_where_hierarchy(bool async) + public override async Task Update_where_using_hierarchy_derived(bool async) { - await base.Update_where_hierarchy(async); + await base.Update_where_using_hierarchy_derived(async); + + AssertExecuteUpdateSql( + """ + UPDATE `Countries` AS `c` + SET `c`.`Name` = 'Monovia' + WHERE ( + SELECT COUNT(*) + FROM `Animals` AS `a` + WHERE (((`a`.`CountryId` = 1) AND (`c`.`Id` = `a`.`CountryId`)) AND (`a`.`Discriminator` = 'Kiwi')) AND (`a`.`CountryId` > 0)) > 0 + """); + } + + public override async Task Update_where_keyless_entity_mapped_to_sql_query(bool async) + { + await base.Update_where_keyless_entity_mapped_to_sql_query(async); + + AssertExecuteUpdateSql(); + } + + public override async Task Update_base_type(bool async) + { + await base.Update_base_type(async); AssertExecuteUpdateSql( """ @@ -155,60 +170,68 @@ public override async Task Update_where_hierarchy(bool async) """); } - public override async Task Update_where_hierarchy_subquery(bool async) + public override async Task Update_base_type_with_OfType(bool async) { - await base.Update_where_hierarchy_subquery(async); + await base.Update_base_type_with_OfType(async); - AssertExecuteUpdateSql(); + AssertExecuteUpdateSql( + """ + UPDATE `Animals` AS `a` + SET `a`.`Name` = 'NewBird' + WHERE (`a`.`CountryId` = 1) AND (`a`.`Discriminator` = 'Kiwi') + """); } - public override async Task Update_where_hierarchy_derived(bool async) + public override async Task Update_base_property_on_derived_type(bool async) { - await base.Update_where_hierarchy_derived(async); + await base.Update_base_property_on_derived_type(async); AssertExecuteUpdateSql( """ UPDATE `Animals` AS `a` -SET `a`.`Name` = 'Kiwi' -WHERE ((`a`.`Discriminator` = 'Kiwi') AND (`a`.`CountryId` = 1)) AND (`a`.`Name` = 'Great spotted kiwi') +SET `a`.`Name` = 'SomeOtherKiwi' +WHERE (`a`.`Discriminator` = 'Kiwi') AND (`a`.`CountryId` = 1) """); } - public override async Task Update_where_using_hierarchy(bool async) + public override async Task Update_derived_property_on_derived_type(bool async) { - await base.Update_where_using_hierarchy(async); + await base.Update_derived_property_on_derived_type(async); AssertExecuteUpdateSql( """ -UPDATE `Countries` AS `c` -SET `c`.`Name` = 'Monovia' -WHERE ( - SELECT COUNT(*) - FROM `Animals` AS `a` - WHERE ((`a`.`CountryId` = 1) AND (`c`.`Id` = `a`.`CountryId`)) AND (`a`.`CountryId` > 0)) > 0 +UPDATE `Animals` AS `a` +SET `a`.`FoundOn` = 0 +WHERE (`a`.`Discriminator` = 'Kiwi') AND (`a`.`CountryId` = 1) """); } - public override async Task Update_where_using_hierarchy_derived(bool async) + public override async Task Update_base_and_derived_types(bool async) { - await base.Update_where_using_hierarchy_derived(async); + await base.Update_base_and_derived_types(async); AssertExecuteUpdateSql( """ -UPDATE `Countries` AS `c` -SET `c`.`Name` = 'Monovia' -WHERE ( - SELECT COUNT(*) - FROM `Animals` AS `a` - WHERE (((`a`.`CountryId` = 1) AND (`c`.`Id` = `a`.`CountryId`)) AND (`a`.`Discriminator` = 'Kiwi')) AND (`a`.`CountryId` > 0)) > 0 +UPDATE `Animals` AS `a` +SET `a`.`FoundOn` = 0, + `a`.`Name` = 'Kiwi' +WHERE (`a`.`Discriminator` = 'Kiwi') AND (`a`.`CountryId` = 1) """); } - public override async Task Update_where_keyless_entity_mapped_to_sql_query(bool async) + [SupportedServerVersionCondition(nameof(ServerVersionSupport.LimitWithinInAllAnySomeSubquery))] + public override async Task Delete_where_hierarchy_subquery(bool async) { - await base.Update_where_keyless_entity_mapped_to_sql_query(async); + await base.Delete_where_hierarchy_subquery(async); - AssertExecuteUpdateSql(); + AssertSql(); + } + + public override async Task Delete_where_keyless_entity_mapped_to_sql_query(bool async) + { + await base.Delete_where_keyless_entity_mapped_to_sql_query(async); + + AssertSql(); } protected override void ClearLog() diff --git a/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/InheritanceBulkUpdatesSingleStoreFixture.cs b/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/TPHInheritanceBulkUpdatesSingleStoreFixture.cs similarity index 92% rename from test/EFCore.SingleStore.FunctionalTests/BulkUpdates/InheritanceBulkUpdatesSingleStoreFixture.cs rename to test/EFCore.SingleStore.FunctionalTests/BulkUpdates/TPHInheritanceBulkUpdatesSingleStoreFixture.cs index ebd6220a2..4f4b05c75 100644 --- a/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/InheritanceBulkUpdatesSingleStoreFixture.cs +++ b/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/TPHInheritanceBulkUpdatesSingleStoreFixture.cs @@ -6,7 +6,7 @@ namespace EntityFrameworkCore.SingleStore.FunctionalTests.BulkUpdates; -public class InheritanceBulkUpdatesSingleStoreFixture : InheritanceBulkUpdatesRelationalFixture +public class TPHInheritanceBulkUpdatesSingleStoreFixture : TPHInheritanceBulkUpdatesFixture { protected override ITestStoreFactory TestStoreFactory => SingleStoreTestStoreFactory.Instance; diff --git a/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/InheritanceBulkUpdatesSingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/TPHInheritanceBulkUpdatesSingleStoreTest.cs similarity index 68% rename from test/EFCore.SingleStore.FunctionalTests/BulkUpdates/InheritanceBulkUpdatesSingleStoreTest.cs rename to test/EFCore.SingleStore.FunctionalTests/BulkUpdates/TPHInheritanceBulkUpdatesSingleStoreTest.cs index 69c38d2d9..6f1a7005b 100644 --- a/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/InheritanceBulkUpdatesSingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/TPHInheritanceBulkUpdatesSingleStoreTest.cs @@ -2,13 +2,15 @@ using Microsoft.EntityFrameworkCore.BulkUpdates; using Microsoft.EntityFrameworkCore.TestUtilities; using SingleStoreConnector; +using EntityFrameworkCore.SingleStore.Infrastructure; +using EntityFrameworkCore.SingleStore.Tests.TestUtilities.Attributes; using Xunit; namespace EntityFrameworkCore.SingleStore.FunctionalTests.BulkUpdates; -public class InheritanceBulkUpdatesSingleStoreTest : InheritanceBulkUpdatesTestBase +public class TPHInheritanceBulkUpdatesSingleStoreTest : TPHInheritanceBulkUpdatesTestBase { - public InheritanceBulkUpdatesSingleStoreTest(InheritanceBulkUpdatesSingleStoreFixture fixture) + public TPHInheritanceBulkUpdatesSingleStoreTest(TPHInheritanceBulkUpdatesSingleStoreFixture fixture) : base(fixture) { ClearLog(); @@ -93,15 +95,16 @@ await Assert.ThrowsAsync( """ DELETE `a` FROM `Animals` AS `a` -WHERE EXISTS ( - SELECT 1 - FROM `Animals` AS `a0` - GROUP BY `a0`.`CountryId` - HAVING (COUNT(*) < 3) AND (( +WHERE `a`.`Id` IN ( +SELECT ( SELECT `a1`.`Id` FROM `Animals` AS `a1` WHERE `a0`.`CountryId` = `a1`.`CountryId` - LIMIT 1) = `a`.`Id`)) + LIMIT 1) + FROM `Animals` AS `a0` + GROUP BY `a0`.`CountryId` + HAVING COUNT(*) < 3 +) """); } @@ -112,6 +115,7 @@ public override async Task Delete_where_keyless_entity_mapped_to_sql_query(bool AssertSql(); } + [SupportedServerVersionCondition(nameof(ServerVersionSupport.LimitWithinInAllAnySomeSubquery))] public override async Task Delete_where_hierarchy_subquery(bool async) { await base.Delete_where_hierarchy_subquery(async); @@ -136,18 +140,6 @@ LIMIT @__p_1 OFFSET @__p_0 """); } - public override async Task Update_where_hierarchy(bool async) - { - await base.Update_where_hierarchy(async); - - AssertExecuteUpdateSql( -""" -UPDATE `Animals` AS `a` -SET `a`.`Name` = 'Animal' -WHERE `a`.`Name` = 'Great spotted kiwi' -"""); - } - public override async Task Update_where_hierarchy_subquery(bool async) { await base.Update_where_hierarchy_subquery(async); @@ -155,18 +147,6 @@ public override async Task Update_where_hierarchy_subquery(bool async) AssertExecuteUpdateSql(); } - public override async Task Update_where_hierarchy_derived(bool async) - { - await base.Update_where_hierarchy_derived(async); - - AssertExecuteUpdateSql( -""" -UPDATE `Animals` AS `a` -SET `a`.`Name` = 'Kiwi' -WHERE (`a`.`Discriminator` = 'Kiwi') AND (`a`.`Name` = 'Great spotted kiwi') -"""); - } - public override async Task Update_where_using_hierarchy(bool async) { await base.Update_where_using_hierarchy(async); @@ -204,6 +184,91 @@ public override async Task Update_where_keyless_entity_mapped_to_sql_query(bool AssertExecuteUpdateSql(); } + public override async Task Update_base_type(bool async) + { + await base.Update_base_type(async); + + AssertExecuteUpdateSql( +""" +UPDATE `Animals` AS `a` +SET `a`.`Name` = 'Animal' +WHERE `a`.`Name` = 'Great spotted kiwi' +"""); + } + + public override async Task Update_base_type_with_OfType(bool async) + { + await base.Update_base_type_with_OfType(async); + + AssertExecuteUpdateSql( +""" +UPDATE `Animals` AS `a` +SET `a`.`Name` = 'NewBird' +WHERE `a`.`Discriminator` = 'Kiwi' +"""); + } + + public override async Task Update_base_property_on_derived_type(bool async) + { + await base.Update_base_property_on_derived_type(async); + + AssertExecuteUpdateSql( +""" +UPDATE `Animals` AS `a` +SET `a`.`Name` = 'SomeOtherKiwi' +WHERE `a`.`Discriminator` = 'Kiwi' +"""); + } + + public override async Task Update_derived_property_on_derived_type(bool async) + { + await base.Update_derived_property_on_derived_type(async); + + AssertExecuteUpdateSql( +""" +UPDATE `Animals` AS `a` +SET `a`.`FoundOn` = 0 +WHERE `a`.`Discriminator` = 'Kiwi' +"""); + } + + public override async Task Update_base_and_derived_types(bool async) + { + await base.Update_base_and_derived_types(async); + + AssertExecuteUpdateSql( +""" +UPDATE `Animals` AS `a` +SET `a`.`FoundOn` = 0, + `a`.`Name` = 'Kiwi' +WHERE `a`.`Discriminator` = 'Kiwi' +"""); + } + + public override async Task Update_with_interface_in_property_expression(bool async) + { + await base.Update_with_interface_in_property_expression(async); + + AssertExecuteUpdateSql( +""" +UPDATE `Drinks` AS `d` +SET `d`.`SugarGrams` = 0 +WHERE `d`.`Discriminator` = 1 +"""); + } + + public override async Task Update_with_interface_in_EF_Property_in_property_expression(bool async) + { + await base.Update_with_interface_in_EF_Property_in_property_expression(async); + + AssertExecuteUpdateSql( +""" +UPDATE `Drinks` AS `d` +SET `d`.`SugarGrams` = 0 +WHERE `d`.`Discriminator` = 1 +"""); + } + protected override void ClearLog() => Fixture.TestSqlLoggerFactory.Clear(); diff --git a/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesSingleStoreFixture.cs b/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesSingleStoreFixture.cs index 2dc8607a5..5cd71adc9 100644 --- a/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesSingleStoreFixture.cs +++ b/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesSingleStoreFixture.cs @@ -8,7 +8,7 @@ public class TPTFiltersInheritanceBulkUpdatesSingleStoreFixture : TPTInheritance protected override string StoreName => "TPTFiltersInheritanceBulkUpdatesTest"; - protected override bool EnableFilters + public override bool EnableFilters => true; protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) diff --git a/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesSingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesSingleStoreTest.cs index a27134852..2a13f3b3b 100644 --- a/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesSingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesSingleStoreTest.cs @@ -3,14 +3,17 @@ using Microsoft.EntityFrameworkCore.BulkUpdates; using Microsoft.EntityFrameworkCore.TestUtilities; using Xunit; +using Xunit.Abstractions; namespace EntityFrameworkCore.SingleStore.FunctionalTests.BulkUpdates; public class TPTFiltersInheritanceBulkUpdatesSingleStoreTest : TPTFiltersInheritanceBulkUpdatesTestBase< TPTFiltersInheritanceBulkUpdatesSingleStoreFixture> { - public TPTFiltersInheritanceBulkUpdatesSingleStoreTest(TPTFiltersInheritanceBulkUpdatesSingleStoreFixture fixture) - : base(fixture) + public TPTFiltersInheritanceBulkUpdatesSingleStoreTest( + TPTFiltersInheritanceBulkUpdatesSingleStoreFixture fixture, + ITestOutputHelper testOutputHelper) + : base(fixture, testOutputHelper) { ClearLog(); } @@ -111,20 +114,6 @@ public override async Task Delete_GroupBy_Where_Select_First_3(bool async) AssertSql(); } - public override async Task Update_where_hierarchy(bool async) - { - // We're skipping this test when we're running tests on Managed Service due to the specifics of - // how AUTO_INCREMENT works (https://docs.singlestore.com/cloud/reference/sql-reference/data-definition-language-ddl/create-table/#auto-increment-behavior) - if (AppConfig.ManagedService) - { - return; - } - - await base.Update_where_hierarchy(async); - - AssertExecuteUpdateSql(); - } - public override async Task Update_where_hierarchy_subquery(bool async) { await base.Update_where_hierarchy_subquery(async); @@ -132,13 +121,6 @@ public override async Task Update_where_hierarchy_subquery(bool async) AssertExecuteUpdateSql(); } - public override async Task Update_where_hierarchy_derived(bool async) - { - await base.Update_where_hierarchy_derived(async); - - AssertExecuteUpdateSql(); - } - public override async Task Update_where_using_hierarchy(bool async) { // We're skipping this test when we're running tests on Managed Service due to the specifics of @@ -196,6 +178,88 @@ public override async Task Update_where_keyless_entity_mapped_to_sql_query(bool AssertExecuteUpdateSql(); } + public override async Task Update_base_and_derived_types(bool async) + { + await base.Update_base_and_derived_types(async); + + AssertExecuteUpdateSql(); + } + + public override async Task Update_base_type(bool async) + { + // We're skipping this test when we're running tests on Managed Service due to the specifics of + // how AUTO_INCREMENT works (https://docs.singlestore.com/cloud/reference/sql-reference/data-definition-language-ddl/create-table/#auto-increment-behavior) + if (AppConfig.ManagedService) + { + return; + } + + await base.Update_base_type(async); + + AssertExecuteUpdateSql( + """ + UPDATE `Animals` AS `a` + SET `a`.`Name` = 'Animal' + WHERE (`a`.`CountryId` = 1) AND (`a`.`Name` = 'Great spotted kiwi') + """); + } + + public override async Task Update_base_type_with_OfType(bool async) + { + // We're skipping this test when we're running tests on Managed Service due to the specifics of + // how AUTO_INCREMENT works (https://docs.singlestore.com/cloud/reference/sql-reference/data-definition-language-ddl/create-table/#auto-increment-behavior) + if (AppConfig.ManagedService) + { + return; + } + + await base.Update_base_type_with_OfType(async); + + AssertExecuteUpdateSql( + """ + UPDATE `Animals` AS `a` + LEFT JOIN `Kiwi` AS `k` ON `a`.`Id` = `k`.`Id` + SET `a`.`Name` = 'NewBird' + WHERE (`a`.`CountryId` = 1) AND `k`.`Id` IS NOT NULL + """); + } + + public override async Task Update_base_property_on_derived_type(bool async) + { + // We're skipping this test when we're running tests on Managed Service due to the specifics of + // how AUTO_INCREMENT works (https://docs.singlestore.com/cloud/reference/sql-reference/data-definition-language-ddl/create-table/#auto-increment-behavior) + if (AppConfig.ManagedService) + { + return; + } + + await base.Update_base_property_on_derived_type(async); + + AssertExecuteUpdateSql( + """ + UPDATE `Animals` AS `a` + INNER JOIN `Birds` AS `b` ON `a`.`Id` = `b`.`Id` + INNER JOIN `Kiwi` AS `k` ON `a`.`Id` = `k`.`Id` + SET `a`.`Name` = 'SomeOtherKiwi' + WHERE `a`.`CountryId` = 1 + """); + } + + [ConditionalTheory(Skip = "Operation 'Update/Delete right table of a join' is not allowed.")] + public override async Task Update_derived_property_on_derived_type(bool async) + { + await base.Update_derived_property_on_derived_type(async); + + AssertExecuteUpdateSql( + """ + UPDATE `Animals` AS `a` + INNER JOIN `Birds` AS `b` ON `a`.`Id` = `b`.`Id` + INNER JOIN `Kiwi` AS `k` ON `a`.`Id` = `k`.`Id` + SET `k`.`FoundOn` = 0 + WHERE `a`.`CountryId` = 1 + """); + } + protected override void ClearLog() => Fixture.TestSqlLoggerFactory.Clear(); diff --git a/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesSingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesSingleStoreTest.cs index ad144c180..117f81abd 100644 --- a/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesSingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesSingleStoreTest.cs @@ -3,13 +3,16 @@ using Microsoft.EntityFrameworkCore.BulkUpdates; using Microsoft.EntityFrameworkCore.TestUtilities; using Xunit; +using Xunit.Abstractions; namespace EntityFrameworkCore.SingleStore.FunctionalTests.BulkUpdates; public class TPTInheritanceBulkUpdatesSingleStoreTest : TPTInheritanceBulkUpdatesTestBase { - public TPTInheritanceBulkUpdatesSingleStoreTest(TPTInheritanceBulkUpdatesSingleStoreFixture fixture) - : base(fixture) + public TPTInheritanceBulkUpdatesSingleStoreTest( + TPTInheritanceBulkUpdatesSingleStoreFixture fixture, + ITestOutputHelper testOutputHelper) + : base(fixture, testOutputHelper) { ClearLog(); } @@ -77,20 +80,6 @@ public override async Task Delete_GroupBy_Where_Select_First_3(bool async) AssertSql(); } - public override async Task Update_where_hierarchy(bool async) - { - // We're skipping this test when we're running tests on Managed Service due to the specifics of - // how AUTO_INCREMENT works (https://docs.singlestore.com/cloud/reference/sql-reference/data-definition-language-ddl/create-table/#auto-increment-behavior) - if (AppConfig.ManagedService) - { - return; - } - - await base.Update_where_hierarchy(async); - - AssertExecuteUpdateSql(); - } - public override async Task Update_where_hierarchy_subquery(bool async) { await base.Update_where_hierarchy_subquery(async); @@ -98,13 +87,6 @@ public override async Task Update_where_hierarchy_subquery(bool async) AssertExecuteUpdateSql(); } - public override async Task Update_where_hierarchy_derived(bool async) - { - await base.Update_where_hierarchy_derived(async); - - AssertExecuteUpdateSql(); - } - public override async Task Update_where_using_hierarchy(bool async) { // We're skipping this test when we're running tests on Managed Service due to the specifics of @@ -162,6 +144,112 @@ public override async Task Update_where_keyless_entity_mapped_to_sql_query(bool AssertExecuteUpdateSql(); } + public override async Task Update_base_and_derived_types(bool async) + { + await base.Update_base_and_derived_types(async); + + AssertExecuteUpdateSql(); + } + + public override async Task Update_base_type(bool async) + { + // We're skipping this test when we're running tests on Managed Service due to the specifics of + // how AUTO_INCREMENT works (https://docs.singlestore.com/cloud/reference/sql-reference/data-definition-language-ddl/create-table/#auto-increment-behavior) + if (AppConfig.ManagedService) + { + return; + } + + await base.Update_base_type(async); + + AssertExecuteUpdateSql( +""" +UPDATE `Animals` AS `a` +SET `a`.`Name` = 'Animal' +WHERE `a`.`Name` = 'Great spotted kiwi' +"""); + } + + public override async Task Update_base_type_with_OfType(bool async) + { + // We're skipping this test when we're running tests on Managed Service due to the specifics of + // how AUTO_INCREMENT works (https://docs.singlestore.com/cloud/reference/sql-reference/data-definition-language-ddl/create-table/#auto-increment-behavior) + if (AppConfig.ManagedService) + { + return; + } + + await base.Update_base_type_with_OfType(async); + + AssertExecuteUpdateSql( +""" +UPDATE `Animals` AS `a` +LEFT JOIN `Kiwi` AS `k` ON `a`.`Id` = `k`.`Id` +SET `a`.`Name` = 'NewBird' +WHERE `k`.`Id` IS NOT NULL +"""); + } + + public override async Task Update_base_property_on_derived_type(bool async) + { + // We're skipping this test when we're running tests on Managed Service due to the specifics of + // how AUTO_INCREMENT works (https://docs.singlestore.com/cloud/reference/sql-reference/data-definition-language-ddl/create-table/#auto-increment-behavior) + if (AppConfig.ManagedService) + { + return; + } + + await base.Update_base_property_on_derived_type(async); + + AssertExecuteUpdateSql( +""" +UPDATE `Animals` AS `a` +INNER JOIN `Birds` AS `b` ON `a`.`Id` = `b`.`Id` +INNER JOIN `Kiwi` AS `k` ON `a`.`Id` = `k`.`Id` +SET `a`.`Name` = 'SomeOtherKiwi' +"""); + } + + [ConditionalTheory(Skip = "Operation 'Update/Delete right table of a join' is not allowed.")] + public override async Task Update_derived_property_on_derived_type(bool async) + { + await base.Update_derived_property_on_derived_type(async); + + AssertExecuteUpdateSql( +""" +UPDATE `Animals` AS `a` +INNER JOIN `Birds` AS `b` ON `a`.`Id` = `b`.`Id` +INNER JOIN `Kiwi` AS `k` ON `a`.`Id` = `k`.`Id` +SET `k`.`FoundOn` = 0 +"""); + } + + [ConditionalTheory(Skip = "Operation 'Update/Delete right table of a join' is not allowed.")] + public override async Task Update_with_interface_in_property_expression(bool async) + { + await base.Update_with_interface_in_property_expression(async); + + AssertExecuteUpdateSql( +""" +UPDATE `Drinks` AS `d` +INNER JOIN `Coke` AS `c` ON `d`.`Id` = `c`.`Id` +SET `c`.`SugarGrams` = 0 +"""); + } + + [ConditionalTheory(Skip = "Operation 'Update/Delete right table of a join' is not allowed.")] + public override async Task Update_with_interface_in_EF_Property_in_property_expression(bool async) + { + await base.Update_with_interface_in_EF_Property_in_property_expression(async); + + AssertExecuteUpdateSql( +""" +UPDATE `Drinks` AS `d` +INNER JOIN `Coke` AS `c` ON `d`.`Id` = `c`.`Id` +SET `c`.`SugarGrams` = 0 +"""); + } + protected override void ClearLog() => Fixture.TestSqlLoggerFactory.Clear(); diff --git a/test/EFCore.SingleStore.FunctionalTests/ComplexTypesTrackingSingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/ComplexTypesTrackingSingleStoreTest.cs new file mode 100644 index 000000000..e044edd1f --- /dev/null +++ b/test/EFCore.SingleStore.FunctionalTests/ComplexTypesTrackingSingleStoreTest.cs @@ -0,0 +1,248 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.TestUtilities; +using EntityFrameworkCore.SingleStore.FunctionalTests.TestUtilities; +using Xunit.Abstractions; + +namespace EntityFrameworkCore.SingleStore.FunctionalTests; + +public class ComplexTypesTrackingSingleStoreTest : ComplexTypesTrackingTestBase +{ + public ComplexTypesTrackingSingleStoreTest(SingleStoreFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + fixture.TestSqlLoggerFactory.Clear(); + fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) + => facade.UseTransaction(transaction.GetDbTransaction()); + + public class SingleStoreFixture : FixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => SingleStoreTestStoreFactory.Instance; + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + // modelBuilder.Entity( + // b => + // { + // b.ComplexProperty( + // e => e.LunchtimeActivity, b => + // { + // b.ComplexProperty(e => e!.Champions, b => b.Ignore(e => e.Members)); + // b.ComplexProperty(e => e!.RunnersUp, b => b.Ignore(e => e.Members)); + // b.Ignore(e => e!.Notes); + // }); + // b.ComplexProperty( + // e => e.EveningActivity, b => + // { + // b.ComplexProperty(e => e.Champions, b => b.Ignore(e => e.Members)); + // b.ComplexProperty(e => e.RunnersUp, b => b.Ignore(e => e.Members)); + // b.Ignore(e => e.Notes); + // }); + // b.ComplexProperty(e => e.FeaturedTeam, b => b.Ignore(e => e.Members)); + // b.ComplexProperty(e => e.FeaturedTeam, b => b.Ignore(e => e.Members)); + // }); + // + // modelBuilder.Entity( + // b => + // { + // b.ComplexProperty( + // e => e.LunchtimeActivity, b => + // { + // b.ComplexProperty(e => e!.Champions, b => b.Ignore(e => e.Members)); + // b.ComplexProperty(e => e!.RunnersUp, b => b.Ignore(e => e.Members)); + // b.Ignore(e => e!.Notes); + // }); + // b.ComplexProperty( + // e => e.EveningActivity, b => + // { + // b.ComplexProperty(e => e.Champions, b => b.Ignore(e => e.Members)); + // b.ComplexProperty(e => e.RunnersUp, b => b.Ignore(e => e.Members)); + // b.Ignore(e => e.Notes); + // }); + // b.ComplexProperty(e => e.FeaturedTeam, b => b.Ignore(e => e.Members)); + // b.ComplexProperty(e => e.FeaturedTeam, b => b.Ignore(e => e.Members)); + // }); + // + // modelBuilder.Entity( + // b => + // { + // b.ComplexProperty( + // e => e.LunchtimeActivity, b => + // { + // b.ComplexProperty(e => e!.Champions, b => b.Ignore(e => e.Members)); + // b.ComplexProperty(e => e!.RunnersUp, b => b.Ignore(e => e.Members)); + // b.Ignore(e => e!.Notes); + // }); + // b.ComplexProperty( + // e => e.EveningActivity, b => + // { + // b.ComplexProperty(e => e.Champions, b => b.Ignore(e => e.Members)); + // b.ComplexProperty(e => e.RunnersUp, b => b.Ignore(e => e.Members)); + // b.Ignore(e => e.Notes); + // }); + // b.ComplexProperty(e => e.FeaturedTeam, b => b.Ignore(e => e.Members)); + // b.ComplexProperty(e => e.FeaturedTeam, b => b.Ignore(e => e.Members)); + // }); + // + // modelBuilder.Entity( + // b => + // { + // b.ComplexProperty( + // e => e.LunchtimeActivity, b => + // { + // b.ComplexProperty(e => e!.Champions, b => b.Ignore(e => e.Members)); + // b.ComplexProperty(e => e!.RunnersUp, b => b.Ignore(e => e.Members)); + // b.Ignore(e => e!.Notes); + // }); + // b.ComplexProperty( + // e => e.EveningActivity, b => + // { + // b.ComplexProperty(e => e.Champions, b => b.Ignore(e => e.Members)); + // b.ComplexProperty(e => e.RunnersUp, b => b.Ignore(e => e.Members)); + // b.Ignore(e => e.Notes); + // }); + // b.ComplexProperty(e => e.FeaturedTeam, b => b.Ignore(e => e.Members)); + // b.ComplexProperty(e => e.FeaturedTeam, b => b.Ignore(e => e.Members)); + // }); + // + // modelBuilder.Entity( + // b => + // { + // b.ComplexProperty( + // e => e.LunchtimeActivity, b => + // { + // b.ComplexProperty(e => e!.Champions, b => b.Ignore(e => e.Members)); + // b.ComplexProperty(e => e!.RunnersUp, b => b.Ignore(e => e.Members)); + // b.Ignore(e => e!.Notes); + // }); + // b.ComplexProperty( + // e => e.EveningActivity, b => + // { + // b.ComplexProperty(e => e.Champions, b => b.Ignore(e => e.Members)); + // b.ComplexProperty(e => e.RunnersUp, b => b.Ignore(e => e.Members)); + // b.Ignore(e => e.Notes); + // }); + // b.ComplexProperty(e => e.FeaturedTeam, b => b.Ignore(e => e.Members)); + // b.ComplexProperty(e => e.FeaturedTeam, b => b.Ignore(e => e.Members)); + // }); + // + // modelBuilder.Entity( + // b => + // { + // b.ComplexProperty( + // e => e.LunchtimeActivity, b => + // { + // b.ComplexProperty(e => e!.Champions, b => b.Ignore(e => e.Members)); + // b.ComplexProperty(e => e!.RunnersUp, b => b.Ignore(e => e.Members)); + // b.Ignore(e => e!.Notes); + // }); + // b.ComplexProperty( + // e => e.EveningActivity, b => + // { + // b.ComplexProperty(e => e.Champions, b => b.Ignore(e => e.Members)); + // b.ComplexProperty(e => e.RunnersUp, b => b.Ignore(e => e.Members)); + // b.Ignore(e => e.Notes); + // }); + // b.ComplexProperty(e => e.FeaturedTeam, b => b.Ignore(e => e.Members)); + // b.ComplexProperty(e => e.FeaturedTeam, b => b.Ignore(e => e.Members)); + // }); + // + // // TODO: Allow binding of complex properties to constructors + // // modelBuilder.Entity( + // // b => + // // { + // // b.ComplexProperty( + // // e => e.LunchtimeActivity, b => + // // { + // // b.ComplexProperty(e => e!.Champions, b => b.Ignore(e => e.Members)); + // // b.ComplexProperty(e => e!.RunnersUp, b => b.Ignore(e => e.Members)); + // // b.Ignore(e => e!.Notes); + // // }); + // // b.ComplexProperty( + // // e => e.EveningActivity, b => + // // { + // // b.ComplexProperty(e => e.Champions, b => b.Ignore(e => e.Members)); + // // b.ComplexProperty(e => e.RunnersUp, b => b.Ignore(e => e.Members)); + // // b.Ignore(e => e.Notes); + // // }); + // // b.ComplexProperty(e => e.FeaturedTeam, b => b.Ignore(e => e.Members)); + // // b.ComplexProperty(e => e.FeaturedTeam, b => b.Ignore(e => e.Members)); + // // }); + // + // modelBuilder.Entity( + // b => + // { + // b.ComplexProperty( + // e => e.LunchtimeActivity, b => + // { + // b.ComplexProperty(e => e!.Champions, b => b.Ignore(e => e.Members)); + // b.ComplexProperty(e => e!.RunnersUp, b => b.Ignore(e => e.Members)); + // b.Ignore(e => e!.Notes); + // }); + // b.ComplexProperty( + // e => e.EveningActivity, b => + // { + // b.ComplexProperty(e => e.Champions, b => b.Ignore(e => e.Members)); + // b.ComplexProperty(e => e.RunnersUp, b => b.Ignore(e => e.Members)); + // b.Ignore(e => e.Notes); + // }); + // b.ComplexProperty(e => e.FeaturedTeam, b => b.Ignore(e => e.Members)); + // b.ComplexProperty(e => e.FeaturedTeam, b => b.Ignore(e => e.Members)); + // }); + + // // TODO: We currently don't support primitive collections yet. + // var entityTypes = modelBuilder.Model.GetEntityTypes().ToArray(); + // var unprocessedComplexProperties = new Queue(entityTypes.SelectMany(e => e.GetComplexProperties())); + // var processedComplexProperties = new HashSet(); + // + // while (unprocessedComplexProperties.TryDequeue(out var complexProperty)) + // { + // processedComplexProperties.Add(complexProperty); + // + // Debug.WriteLine(complexProperty.ClrType.Name); + // + // var unmappedProperties = complexProperty.ComplexType.ClrType + // .GetProperties() + // .Except( + // complexProperty.ComplexType + // .GetProperties() + // .Where(p => p.PropertyInfo is not null) + // .Select(p => p.PropertyInfo)); + // + // foreach (var property in unmappedProperties) + // { + // + // // if (propertyInfo.IsGenericType && + // // propertyInfo.GetGenericTypeDefinition() == typeof(List<>)) + // // { + // // Debug.WriteLine($"{propertyInfo.Name} {complexProperty.ClrType.Name}.{property.Name}"); + // // } + // } + // + // foreach (var innerComplexProperty in complexProperty.ComplexType.GetComplexProperties()) + // { + // if (!processedComplexProperties.Contains(innerComplexProperty) && + // !unprocessedComplexProperties.Contains(innerComplexProperty)) + // { + // unprocessedComplexProperties.Enqueue(innerComplexProperty); + // } + // } + // } + } + } +} diff --git a/test/EFCore.SingleStore.FunctionalTests/ConnectionSingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/ConnectionSingleStoreTest.cs index afa471d82..3ecc2551b 100644 --- a/test/EFCore.SingleStore.FunctionalTests/ConnectionSingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/ConnectionSingleStoreTest.cs @@ -1,9 +1,13 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Reflection; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.DependencyInjection; using SingleStoreConnector; using EntityFrameworkCore.SingleStore.FunctionalTests.TestUtilities; +using EntityFrameworkCore.SingleStore.Storage.Internal; using EntityFrameworkCore.SingleStore.Tests; using Xunit; @@ -68,6 +72,7 @@ public virtual void ConnectionAttributes() { dbConnAttrs.Add(new Tuple(reader.GetString(0), reader.GetString(1))); } + Tuple expectedPair = new Tuple("my_key_1", "my_val_1"); Assert.Contains(expectedPair, dbConnAttrs); expectedPair = new Tuple("my_key_2", "my_val_2"); @@ -75,28 +80,126 @@ public virtual void ConnectionAttributes() } } + [Fact] + public void UseSingleStore_IncludesConnectorAttributes_InConnectionString() + { + using var _ = ((SingleStoreTestStore)SingleStoreNorthwindTestStoreFactory.Instance + .GetOrCreate("ConnectionAttributesTest")) + .Initialize(null, (Func)null); + + var cs = SingleStoreTestStore.CreateConnectionString("ConnectionAttributesTest"); + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseSingleStore(cs, b => b.ApplyConfiguration()); + + using var context = new GeneralOptionsContext(optionsBuilder.Options); + + var conn = (SingleStoreConnection)context.Database.GetDbConnection(); + var csb = new SingleStoreConnectionStringBuilder(conn.ConnectionString); + + var parts = csb.ConnectionAttributes + .TrimEnd(',') + .Split(',', StringSplitOptions.RemoveEmptyEntries) + .Select(p => p.Trim()) + .ToArray(); + + var programVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString(); + Assert.Contains(parts, p => p.StartsWith("_connector_name:SingleStore Entity Framework Core provider")); + Assert.Contains(parts, p => p.StartsWith($"_connector_version:{programVersion}")); + } + + [Fact] + public void Can_create_admin_connection_with_data_source() + { + using var _ = ((SingleStoreTestStore)SingleStoreNorthwindTestStoreFactory.Instance + .GetOrCreate("ConnectionTest")) + .Initialize(null, (Func)null); + + using var dataSource = new SingleStoreDataSourceBuilder(SingleStoreTestStore.CreateConnectionString("ConnectionTest")).Build(); + + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseSingleStore(dataSource, b => b.ApplyConfiguration()); + using var context = new GeneralOptionsContext(optionsBuilder.Options); + + var relationalConnection = context.GetService(); + using var masterConnection = relationalConnection.CreateMasterConnection(); + + Assert.Equal(string.Empty, new SingleStoreConnectionStringBuilder(masterConnection.ConnectionString).Database); + + masterConnection.Open(); + } + + [Fact] + public void Can_create_admin_connection_with_connection_string() + { + using var _ = ((SingleStoreTestStore)SingleStoreNorthwindTestStoreFactory.Instance + .GetOrCreate("ConnectionTest")) + .Initialize(null, (Func)null); + + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseSingleStore(SingleStoreTestStore.CreateConnectionString("ConnectionTest"), + b => b.ApplyConfiguration()); + using var context = new GeneralOptionsContext(optionsBuilder.Options); + + var relationalConnection = context.GetService(); + using var masterConnection = relationalConnection.CreateMasterConnection(); + + Assert.Equal(string.Empty, new SingleStoreConnectionStringBuilder(masterConnection.ConnectionString).Database); + + masterConnection.Open(); + } + + [Fact] + public void Can_create_admin_connection_with_connection() + { + using var _ = ((SingleStoreTestStore)SingleStoreNorthwindTestStoreFactory.Instance + .GetOrCreate("ConnectionTestWithConnection")) + .Initialize(null, (Func)null); + + using var connection = new SingleStoreConnection(SingleStoreTestStore.CreateConnectionString("ConnectionTestWithConnection")); + + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseSingleStore(connection, b => b.ApplyConfiguration()); + using var context = new GeneralOptionsContext(optionsBuilder.Options); + + var relationalConnection = context.GetService(); + using var masterConnection = relationalConnection.CreateMasterConnection(); + + Assert.Equal(string.Empty, new SingleStoreConnectionStringBuilder(masterConnection.ConnectionString).Database); + + masterConnection.Open(); + } + private readonly IServiceProvider _serviceProvider = new ServiceCollection() .AddEntityFrameworkSingleStore() .BuildServiceProvider(); - protected ConnectionMysqlContext CreateContext(string connectionString) - => new ConnectionMysqlContext(_serviceProvider, connectionString); - } + protected ConnectionSingleStoreContext CreateContext(string connectionString) + => new ConnectionSingleStoreContext(_serviceProvider, connectionString); - public class ConnectionMysqlContext : DbContext - { - private readonly IServiceProvider _serviceProvider; - private readonly string _connectionString; - public ConnectionMysqlContext(IServiceProvider serviceProvider, string connectionString) + public class ConnectionSingleStoreContext : DbContext { - _serviceProvider = serviceProvider; - _connectionString = connectionString; + private readonly IServiceProvider _serviceProvider; + private readonly string _connectionString; + + public ConnectionSingleStoreContext(IServiceProvider serviceProvider, string connectionString) + { + _serviceProvider = serviceProvider; + _connectionString = connectionString; + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder + .UseSingleStore(_connectionString) + .UseInternalServiceProvider(_serviceProvider); } - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - => optionsBuilder - .UseSingleStore(_connectionString) - .UseInternalServiceProvider(_serviceProvider); + public class GeneralOptionsContext : DbContext + { + public GeneralOptionsContext(DbContextOptions options) + : base(options) + { + } + } } } diff --git a/test/EFCore.SingleStore.FunctionalTests/ConvertToProviderTypesSingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/ConvertToProviderTypesSingleStoreTest.cs index 07040f36f..ba58d25d1 100644 --- a/test/EFCore.SingleStore.FunctionalTests/ConvertToProviderTypesSingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/ConvertToProviderTypesSingleStoreTest.cs @@ -23,31 +23,31 @@ public ConvertToProviderTypesSingleStoreTest(ConvertToProviderTypesSingleStoreFi //fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } - [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore.")] + [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore Distributed.")] public override void Can_read_back_mapped_enum_from_collection_first_or_default() { base.Can_read_back_mapped_enum_from_collection_first_or_default(); } - [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore.")] + [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore Distributed.")] public override void Can_insert_and_read_back_with_binary_key() { base.Can_insert_and_read_back_with_binary_key(); } - [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore.")] + [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore Distributed.")] public override void Can_insert_and_read_back_with_null_binary_foreign_key() { base.Can_insert_and_read_back_with_null_binary_foreign_key(); } - [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore.")] + [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore Distributed.")] public override void Can_insert_and_read_back_with_null_string_foreign_key() { base.Can_insert_and_read_back_with_null_string_foreign_key(); } - [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore.")] + [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore Distributed.")] public override void Can_insert_and_read_back_with_string_key() { base.Can_insert_and_read_back_with_string_key(); @@ -478,7 +478,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con { base.OnModelCreating(modelBuilder, context); - //FOREIGN KEY feature isn't supported by SingleStore + //FOREIGN KEY feature isn't supported by SingleStore Distributed modelBuilder.Ignore(); modelBuilder.Ignore(); } diff --git a/test/EFCore.SingleStore.FunctionalTests/CustomConvertersSingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/CustomConvertersSingleStoreTest.cs index af143ac0a..500a39d65 100644 --- a/test/EFCore.SingleStore.FunctionalTests/CustomConvertersSingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/CustomConvertersSingleStoreTest.cs @@ -4,6 +4,7 @@ using System.Linq.Expressions; using EntityFrameworkCore.SingleStore.FunctionalTests.TestUtilities; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.TestUtilities; @@ -36,37 +37,37 @@ public override void Optional_datetime_reading_null_from_database() // Recheck/remove after `https://github.com/dotnet/efcore/issues/26068` has been fixed upstream. #region https://github.com/dotnet/efcore/issues/26068 - [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore.")] + [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore Distributed.")] public override void Can_insert_and_read_back_with_binary_key() { base.Can_insert_and_read_back_with_binary_key(); } - [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore.")] + [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore Distributed.")] public override void Can_insert_and_read_back_with_null_binary_foreign_key() { base.Can_insert_and_read_back_with_null_binary_foreign_key(); } - [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore.")] + [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore Distributed.")] public override void Can_insert_and_read_back_with_null_string_foreign_key() { base.Can_insert_and_read_back_with_null_string_foreign_key(); } - [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore.")] + [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore Distributed.")] public override void Can_insert_and_read_back_with_string_key() { base.Can_insert_and_read_back_with_string_key(); } - [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore.")] + [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore Distributed.")] public override void Can_read_back_mapped_enum_from_collection_first_or_default() { base.Can_read_back_mapped_enum_from_collection_first_or_default(); } - [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore.")] + [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore Distributed.")] public override void Can_insert_and_read_back_with_case_insensitive_string_key() { base.Can_insert_and_read_back_with_case_insensitive_string_key(); @@ -478,7 +479,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con { base.OnModelCreating(modelBuilder, context); - //FOREIGN KEY feature isn't supported by SingleStore + //FOREIGN KEY feature isn't supported by SingleStore Distributed modelBuilder.Ignore(); modelBuilder.Ignore(); } diff --git a/test/EFCore.SingleStore.FunctionalTests/DataAnnotationSingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/DataAnnotationSingleStoreTest.cs index 9e08fe401..7479bbe84 100644 --- a/test/EFCore.SingleStore.FunctionalTests/DataAnnotationSingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/DataAnnotationSingleStoreTest.cs @@ -91,7 +91,7 @@ public override IModel Timestamp_takes_precedence_over_MaxLength() { var model = base.Timestamp_takes_precedence_over_MaxLength(); - var property = GetProperty(model, "MaxTimestamp"); + var property = GetProperty(model, "MaxTimestamp"); var storeTypeNameBase = property.GetRelationalTypeMapping().StoreTypeNameBase; diff --git a/test/EFCore.SingleStore.FunctionalTests/DatabindingSingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/DatabindingSingleStoreTest.cs index e8374dd2d..36d6f0006 100644 --- a/test/EFCore.SingleStore.FunctionalTests/DatabindingSingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/DatabindingSingleStoreTest.cs @@ -3,16 +3,11 @@ namespace EntityFrameworkCore.SingleStore.FunctionalTests { - public class DatabindingSingleStoreTest : DatabindingTestBase + public class DatabindingSingleStoreTest : DataBindingTestBase { public DatabindingSingleStoreTest(F1SingleStoreFixture fixture) : base(fixture) { } - - public override void DbSet_Local_calls_DetectChanges() - { - base.DbSet_Local_calls_DetectChanges(); - } } } diff --git a/test/EFCore.SingleStore.FunctionalTests/EFCore.SingleStore.FunctionalTests.csproj b/test/EFCore.SingleStore.FunctionalTests/EFCore.SingleStore.FunctionalTests.csproj index 7916c4d9b..74b52e0e5 100644 --- a/test/EFCore.SingleStore.FunctionalTests/EFCore.SingleStore.FunctionalTests.csproj +++ b/test/EFCore.SingleStore.FunctionalTests/EFCore.SingleStore.FunctionalTests.csproj @@ -10,6 +10,7 @@ $(MSBuildWarningsAsMessages);$(NoWarn) + $(DefineConstants);FIXED_TEST_ORDER @@ -17,6 +18,10 @@ + + + + diff --git a/test/EFCore.SingleStore.FunctionalTests/ExistingConnectionSingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/ExistingConnectionSingleStoreTest.cs index a3b3db720..ffec5067f 100644 --- a/test/EFCore.SingleStore.FunctionalTests/ExistingConnectionSingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/ExistingConnectionSingleStoreTest.cs @@ -2,6 +2,8 @@ using System.Data; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.DependencyInjection; using SingleStoreConnector; using EntityFrameworkCore.SingleStore.FunctionalTests.TestUtilities; @@ -119,14 +121,19 @@ private static async Task Opened_connection_missing_AllowUserVariables_true_in_o { await connection.OpenAsync(); + var options = new DbContextOptionsBuilder() + .UseSingleStore(connection) + .Options; + + await using var context = new NorthwindContext(options); + Assert.Equal( Assert.Throws( () => { - new DbContextOptionsBuilder() - .UseSingleStore(connection); + context.GetService(); }).Message, - @"The connection string of a connection used by EntityFrameworkCore.SingleStore must contain ""AllowUserVariables=true;UseAffectedRows=false""."); + @"The connection string of a connection used by EntityFrameworkCore.SingleStore must contain ""AllowUserVariables=True;UseAffectedRows=False""."); } } } @@ -175,14 +182,19 @@ private static async Task Opened_connection_missing_UseAffectedRows_false_in_ori { await connection.OpenAsync(); + var options = new DbContextOptionsBuilder() + .UseSingleStore(connection) + .Options; + + await using var context = new NorthwindContext(options); + Assert.Equal( Assert.Throws( () => { - new DbContextOptionsBuilder() - .UseSingleStore(connection); + context.GetService(); }).Message, - @"The connection string of a connection used by EntityFrameworkCore.SingleStore must contain ""AllowUserVariables=true;UseAffectedRows=false""."); + @"The connection string of a connection used by EntityFrameworkCore.SingleStore must contain ""AllowUserVariables=True;UseAffectedRows=False""."); } } } diff --git a/test/EFCore.SingleStore.FunctionalTests/FullMigrationsSingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/FullMigrationsSingleStoreTest.cs index a6b0896e1..a82d0d344 100644 --- a/test/EFCore.SingleStore.FunctionalTests/FullMigrationsSingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/FullMigrationsSingleStoreTest.cs @@ -164,7 +164,7 @@ INTO PRIMARY_KEY_COLUMN_NAME ignoreLineEndingDifferences: true); } - [ConditionalFact(Skip = "Feature 'DROP PRIMARY KEY' is not supported by SingleStore")] + [ConditionalFact(Skip = "Feature 'DROP PRIMARY KEY' is not supported by SingleStore Distributed")] public virtual void Can_generate_idempotent_up_scripts_with_primary_key_related_stored_procedures() { using var db = Fixture.CreateContext(); @@ -371,7 +371,7 @@ IF NOT EXISTS(SELECT 1 FROM `__EFMigrationsHistory` WHERE `MigrationId` = '00000 ignoreLineEndingDifferences: true); } - [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore.")] + [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore Distributed.")] public virtual void Drop_primary_key_with_recreating_foreign_keys() { using var db = Fixture.CreateContext(); diff --git a/test/EFCore.SingleStore.FunctionalTests/GraphUpdatesSingleStoreTestBase.cs b/test/EFCore.SingleStore.FunctionalTests/GraphUpdatesSingleStoreTestBase.cs index 461da0199..93731f89f 100644 --- a/test/EFCore.SingleStore.FunctionalTests/GraphUpdatesSingleStoreTestBase.cs +++ b/test/EFCore.SingleStore.FunctionalTests/GraphUpdatesSingleStoreTestBase.cs @@ -47,6 +47,20 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con b.HasOne(e => e.UserState).WithMany(e => e.Users).HasForeignKey(e => e.IdUserState); }); + modelBuilder.Entity( + b => + { + b.Property(e => e.AccessStateWithSentinelId).ValueGeneratedNever(); + b.HasData(new AccessStateWithSentinel { AccessStateWithSentinelId = 1 }); + }); + + modelBuilder.Entity( + b => + { + b.Property(e => e.IdUserState).HasDefaultValue(1).Metadata.Sentinel = 667; + b.HasOne(e => e.UserState).WithMany(e => e.Users).HasForeignKey(e => e.IdUserState); + }); + modelBuilder.Entity().Property("CategoryId").HasDefaultValue(1); modelBuilder.Entity().Property(e => e.CategoryId).HasDefaultValue(2); } diff --git a/test/EFCore.SingleStore.FunctionalTests/LazyLoadProxySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/LazyLoadProxySingleStoreTest.cs index f66052c12..8dc35cec2 100644 --- a/test/EFCore.SingleStore.FunctionalTests/LazyLoadProxySingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/LazyLoadProxySingleStoreTest.cs @@ -16,19 +16,21 @@ public LazyLoadProxySingleStoreTest(LoadSingleStoreFixture fixture) public override void Top_level_projection_track_entities_before_passing_to_client_method() { base.Top_level_projection_track_entities_before_passing_to_client_method(); - RecordLog(); Assert.Equal( - @"SELECT `p`.`Id`, `p`.`AlternateId`, `p`.`Discriminator` +""" +SELECT `p`.`Id`, `p`.`AlternateId`, `p`.`Discriminator`, `p`.`Culture_Rating`, `p`.`Culture_Species`, `p`.`Culture_Subspecies`, `p`.`Culture_Validation`, `p`.`Culture_License_Charge`, `p`.`Culture_License_Title`, `p`.`Culture_License_Tag_Text`, `p`.`Culture_License_Tog_Text`, `p`.`Culture_Manufacturer_Name`, `p`.`Culture_Manufacturer_Rating`, `p`.`Culture_Manufacturer_Tag_Text`, `p`.`Culture_Manufacturer_Tog_Text`, `p`.`Milk_Rating`, `p`.`Milk_Species`, `p`.`Milk_Subspecies`, `p`.`Milk_Validation`, `p`.`Milk_License_Charge`, `p`.`Milk_License_Title`, `p`.`Milk_License_Tag_Text`, `p`.`Milk_License_Tog_Text`, `p`.`Milk_Manufacturer_Name`, `p`.`Milk_Manufacturer_Rating`, `p`.`Milk_Manufacturer_Tag_Text`, `p`.`Milk_Manufacturer_Tog_Text` FROM `Parent` AS `p` ORDER BY `p`.`Id` LIMIT 1 @__p_0='707' (Nullable = true) -SELECT `s`.`Id`, `s`.`ParentId` +SELECT `s`.`Id`, `s`.`ParentId`, `s`.`Culture_Rating`, `s`.`Culture_Species`, `s`.`Culture_Subspecies`, `s`.`Culture_Validation`, `s`.`Culture_License_Charge`, `s`.`Culture_License_Title`, `s`.`Culture_License_Tag_Text`, `s`.`Culture_License_Tog_Text`, `s`.`Culture_Manufacturer_Name`, `s`.`Culture_Manufacturer_Rating`, `s`.`Culture_Manufacturer_Tag_Text`, `s`.`Culture_Manufacturer_Tog_Text`, `s`.`Milk_Rating`, `s`.`Milk_Species`, `s`.`Milk_Subspecies`, `s`.`Milk_Validation`, `s`.`Milk_License_Charge`, `s`.`Milk_License_Title`, `s`.`Milk_License_Tag_Text`, `s`.`Milk_License_Tog_Text`, `s`.`Milk_Manufacturer_Name`, `s`.`Milk_Manufacturer_Rating`, `s`.`Milk_Manufacturer_Tag_Text`, `s`.`Milk_Manufacturer_Tog_Text` FROM `Single` AS `s` -WHERE `s`.`ParentId` = @__p_0", +WHERE `s`.`ParentId` = @__p_0 +LIMIT 1 +""", Sql, ignoreLineEndingDifferences: true); } @@ -41,6 +43,17 @@ protected override void RecordLog() => private string Sql { get; set; } + #region Expected JSON override + + // TODO: Tiny discrepancy in decimal representation (Charge: 1.0000000000000000000000000000 instead of 1.00) + protected override string SerializedBlogs1 + => base.SerializedBlogs1.Replace("1.00", "1.0000000000000000000000000000"); + + protected override string SerializedBlogs2 + => base.SerializedBlogs2.Replace("1.00", "1.0000000000000000000000000000"); + + #endregion Expected JSON override + public class LoadSingleStoreFixture : LoadFixtureBase { public TestSqlLoggerFactory TestSqlLoggerFactory diff --git a/test/EFCore.SingleStore.FunctionalTests/MaterializationInterceptionSingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/MaterializationInterceptionSingleStoreTest.cs index 6b2eb2e6a..22aebb204 100644 --- a/test/EFCore.SingleStore.FunctionalTests/MaterializationInterceptionSingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/MaterializationInterceptionSingleStoreTest.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -6,11 +7,13 @@ using Microsoft.Extensions.DependencyInjection; using EntityFrameworkCore.SingleStore.FunctionalTests.TestUtilities; using EntityFrameworkCore.SingleStore.Storage.Internal; +using EntityFrameworkCore.SingleStore.Tests; using Xunit; namespace EntityFrameworkCore.SingleStore.FunctionalTests; -public class MaterializationInterceptionSingleStoreTest : MaterializationInterceptionTestBase, +public class MaterializationInterceptionSingleStoreTest : + MaterializationInterceptionTestBase, IClassFixture { public MaterializationInterceptionSingleStoreTest(MaterializationInterceptionSingleStoreFixture fixture) @@ -18,6 +21,52 @@ public MaterializationInterceptionSingleStoreTest(MaterializationInterceptionSin { } + [ConditionalTheory] + public override async Task Intercept_query_materialization_with_owned_types_projecting_collection(bool async) + { + // We're skipping this test when we're running tests on Managed Service due to the specifics of + // how AUTO_INCREMENT works (https://docs.singlestore.com/cloud/reference/sql-reference/data-definition-language-ddl/create-table/#auto-increment-behavior) + if (AppConfig.ManagedService) + { + return; + } + + await base.Intercept_query_materialization_with_owned_types_projecting_collection(async); + } + + public class SingleStoreLibraryContext : LibraryContext + { + public SingleStoreLibraryContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + // We're changing the data type of the fields from INT to BIGINT, because in SingleStore + // on a sharded (distributed) table, AUTO_INCREMENT can only be used on a BIGINT column + modelBuilder.Entity() + .Property(e => e.Id) + .HasColumnType("bigint"); + + modelBuilder.Entity().OwnsMany( + e => e.Settings, b => + { + b.Property("Id").HasColumnType("bigint"); + }); + + modelBuilder.Entity().OwnsMany(e => e.Settings); + + // TODO: https://github.com/npgsql/efcore.pg/issues/2548 + // modelBuilder.Entity().OwnsMany(e => e.Settings, b => b.ToJson()); + } + } + + public override LibraryContext CreateContext(IEnumerable interceptors, bool inject) + => new SingleStoreLibraryContext(Fixture.CreateOptions(interceptors, inject)); + public class MaterializationInterceptionSingleStoreFixture : SingletonInterceptorsFixtureBase { protected override string StoreName diff --git a/test/EFCore.SingleStore.FunctionalTests/MigrationSqlGeneratorSingleStoreTest.SingleStore.cs b/test/EFCore.SingleStore.FunctionalTests/MigrationSqlGeneratorSingleStoreTest.SingleStore.cs index 6d31380d7..51c223ad7 100644 --- a/test/EFCore.SingleStore.FunctionalTests/MigrationSqlGeneratorSingleStoreTest.SingleStore.cs +++ b/test/EFCore.SingleStore.FunctionalTests/MigrationSqlGeneratorSingleStoreTest.SingleStore.cs @@ -25,7 +25,7 @@ public virtual void DropUniqueConstraintOperation() AssertSql(@"ALTER TABLE `Cars` DROP KEY `AK_Cars_LicensePlateNumber`;"); } - [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore.")] + [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore Distributed.")] public virtual void SingleStoreDropUniqueConstraintAndRecreateForeignKeysOperation_temporarily_drops_foreign_keys() { // A foreign key might reuse the alternate key for its own purposes and prohibit its deletion, @@ -65,7 +65,7 @@ public virtual void DropPrimaryKeyOperation() ALTER TABLE `Cars` DROP PRIMARY KEY;"); } - [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore.")] + [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore Distributed.")] public virtual void SingleStoreDropPrimaryKeyAndRecreateForeignKeysOperation_temporarily_drops_foreign_keys() { // A foreign key might reuse the primary key for its own purposes and prohibit its deletion, diff --git a/test/EFCore.SingleStore.FunctionalTests/MigrationsSingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/MigrationsSingleStoreTest.cs index a2d67ffa6..7d8f2784f 100644 --- a/test/EFCore.SingleStore.FunctionalTests/MigrationsSingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/MigrationsSingleStoreTest.cs @@ -79,9 +79,9 @@ public override Task Alter_column_make_computed(bool? stored) } [ConditionalTheory(Skip = "TODO")] - public override Task Add_column_computed_with_collation() + public override Task Add_column_computed_with_collation(bool stored) { - return base.Add_column_computed_with_collation(); + return base.Add_column_computed_with_collation(stored); } [ConditionalFact(Skip = "BLOB/TEXT columns can't have a default value in SingleStore.")] @@ -364,27 +364,7 @@ public override Task Alter_column_set_collation() [SupportedServerVersionCondition(nameof(ServerVersionSupport.Sequences))] public override async Task Alter_sequence_all_settings() { - await Test( - builder => builder.HasSequence("foo"), - builder => { }, - builder => builder.HasSequence("foo") - .StartsAt(-3) - .IncrementsBy(2) - .HasMin(-5) - .HasMax(10) - .IsCyclic(), - model => - { - var sequence = Assert.Single(model.Sequences); - - // Assert.Equal(-3, sequence.StartValue); - Assert.Equal(1, sequence.StartValue); // Restarting doesn't change the scaffolded start value - - Assert.Equal(2, sequence.IncrementBy); - Assert.Equal(-5, sequence.MinValue); - Assert.Equal(10, sequence.MaxValue); - Assert.True(sequence.IsCyclic); - }); + await base.Alter_sequence_all_settings(); AssertSql( """ @@ -392,7 +372,7 @@ await Test( """, // """ -ALTER SEQUENCE `foo` RESTART WITH -3; +ALTER SEQUENCE `foo` START WITH -3 RESTART; """); } @@ -402,6 +382,15 @@ public override Task Alter_sequence_increment_by() return base.Alter_sequence_increment_by(); } + [SupportedServerVersionCondition(nameof(ServerVersionSupport.Sequences))] + public override async Task Alter_sequence_restart_with() + { + await base.Alter_sequence_restart_with(); + + AssertSql( + @"ALTER SEQUENCE `foo` START WITH 3 RESTART;"); + } + [ConditionalFact(Skip = "SingleStore's ALTER TABLE command doesn't work with comments")] public override Task Alter_table_add_comment() { @@ -621,12 +610,35 @@ public override Task Move_table() [SupportedServerVersionCondition(nameof(ServerVersionSupport.Sequences))] public override async Task Rename_sequence() { - await base.Rename_sequence(); + if (OperatingSystem.IsWindows()) + { + // On Windows, with `lower_case_table_names = 2`, renaming `TestSequence` to `testsequence` doesn't do anything, because + // `TestSequence` is internally being transformed to lower case, before it is processes further. + await Test( + builder => { }, + builder => builder.HasSequence("TestSequence"), + builder => builder.HasSequence("testsequence2"), + builder => builder.RenameSequence(name: "TestSequence", newName: "testsequence2"), + model => + { + var sequence = Assert.Single(model.Sequences); + Assert.Equal("testsequence2", sequence.Name); + }); - AssertSql( -""" -ALTER TABLE `TestSequence` RENAME `testsequence`; -"""); + AssertSql( + """ + ALTER TABLE `TestSequence` RENAME `testsequence2`; + """); + } + else + { + await base.Rename_sequence(); + + AssertSql( + """ + ALTER TABLE `TestSequence` RENAME `testsequence`; + """); + } } [ConditionalTheory(Skip = "TODO")] @@ -718,7 +730,7 @@ await Test( $@"ALTER TABLE `IceCream` ADD `Name` longtext COLLATE {DefaultCollation} NULL;"); } - [ConditionalFact(Skip = "NVARCHAR data type isn't supported by SingleStore.")] + [ConditionalFact(Skip = "NVARCHAR data type isn't supported by SingleStore Distributed.")] public virtual async Task Create_table_NVARCHAR_UPPERCASE_column() { await Test( @@ -1075,7 +1087,7 @@ public virtual void Upgrade_legacy_charset_to_annotation_charset_only_does_not_g Assert.Empty(operations); } - [ConditionalFact] + [ConditionalFact(Skip = "SingleStore works another way.")] public virtual async Task Create_table_explicit_column_charset_takes_precedence_over_inherited_collation() { await Test( @@ -1234,7 +1246,7 @@ await Test( $@"ALTER TABLE `IceCream` MODIFY COLUMN `Brand` longtext CHARACTER SET {NonDefaultCharSet2} NULL;"); } - [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore.")] + [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore Distributed.")] public virtual async Task Drop_unique_constraint_without_recreating_foreign_keys() { await Test( @@ -1266,7 +1278,7 @@ await Test( @"ALTER TABLE `Foo` DROP KEY `AK_Foo_FooAK`;"); } - [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore.")] + [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore Distributed.")] public virtual async Task Drop_unique_constraint_without_recreating_foreign_keys_MigrationBuilder() { await Test( @@ -1299,7 +1311,7 @@ await Test( @"ALTER TABLE `Foo` DROP KEY `AK_Foo_FooAK`;"); } - [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore.")] + [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore Distributed.")] public virtual async Task Drop_unique_constraint_with_recreating_foreign_keys_MigrationBuilder() { await Test( @@ -1336,7 +1348,7 @@ await Test( @"ALTER TABLE `Foo` ADD CONSTRAINT `FK_Foo_Bar_BarFK` FOREIGN KEY (`BarFK`) REFERENCES `Bar` (`BarPK`) ON DELETE CASCADE;"); } - [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore.")] + [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore Distributed.")] public override async Task Add_foreign_key() { await base.Add_foreign_key(); @@ -1347,25 +1359,25 @@ public override async Task Add_foreign_key() @"ALTER TABLE `Orders` ADD CONSTRAINT `FK_Orders_Customers_CustomerId` FOREIGN KEY (`CustomerId`) REFERENCES `Customers` (`Id`) ON DELETE CASCADE;"); } - [ConditionalFact(Skip = "Feature 'Check constraints' is not supported by SingleStore.")] + [ConditionalFact(Skip = "Feature 'Check constraints' is not supported by SingleStore Distributed.")] public override Task Add_check_constraint_with_name() { return base.Add_check_constraint_with_name(); } - [ConditionalFact(Skip = "Feature 'Check constraints' is not supported by SingleStore.")] + [ConditionalFact(Skip = "Feature 'Check constraints' is not supported by SingleStore Distributed.")] public override Task Add_column_with_check_constraint() { return base.Add_column_with_check_constraint(); } - [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore.")] + [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore Distributed.")] public override Task Add_foreign_key_with_name() { return base.Add_foreign_key_with_name(); } - [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore.")] + [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore Distributed.")] public override Task Drop_foreign_key() { return base.Drop_foreign_key(); diff --git a/test/EFCore.SingleStore.FunctionalTests/ModelBuilding101SingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/ModelBuilding101SingleStoreTest.cs new file mode 100644 index 000000000..e88ff94fc --- /dev/null +++ b/test/EFCore.SingleStore.FunctionalTests/ModelBuilding101SingleStoreTest.cs @@ -0,0 +1,10 @@ +using Microsoft.EntityFrameworkCore; +using EntityFrameworkCore.SingleStore.Tests; + +namespace EntityFrameworkCore.SingleStore.FunctionalTests; + +public class ModelBuilding101SingleStoreTest : ModelBuilding101RelationalTestBase +{ + protected override DbContextOptionsBuilder ConfigureContext(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseSingleStore(); +} diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/CaseSensitiveNorthwindQuerySingleStoreFixture.cs b/test/EFCore.SingleStore.FunctionalTests/Query/CaseSensitiveNorthwindQuerySingleStoreFixture.cs index 5fc9626d6..7fbbf7a5d 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/CaseSensitiveNorthwindQuerySingleStoreFixture.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/CaseSensitiveNorthwindQuerySingleStoreFixture.cs @@ -1,12 +1,11 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.TestUtilities; using EntityFrameworkCore.SingleStore.FunctionalTests.TestUtilities; namespace EntityFrameworkCore.SingleStore.FunctionalTests.Query { - public class CaseSensitiveNorthwindQuerySingleStoreFixture : NorthwindQueryRelationalFixture + public class CaseSensitiveNorthwindQuerySingleStoreFixture : NorthwindQuerySingleStoreFixture where TModelCustomizer : IModelCustomizer, new() { protected override string StoreName => "NorthwindCs"; diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/ComplexNavigationsCollectionsQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/ComplexNavigationsCollectionsQuerySingleStoreTest.cs index d33afbce7..045e19e5f 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/ComplexNavigationsCollectionsQuerySingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/ComplexNavigationsCollectionsQuerySingleStoreTest.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.Query; +using EntityFrameworkCore.SingleStore.FunctionalTests.TestUtilities; using EntityFrameworkCore.SingleStore.Infrastructure; using EntityFrameworkCore.SingleStore.Tests; using EntityFrameworkCore.SingleStore.Tests.TestUtilities.Attributes; @@ -132,13 +133,15 @@ public override async Task Include_collection_with_conditional_order_by(bool asy await base.Include_collection_with_conditional_order_by(async); AssertSql( - @"SELECT `l`.`Id`, `l`.`Date`, `l`.`Name`, `l`.`OneToMany_Optional_Self_Inverse1Id`, `l`.`OneToMany_Required_Self_Inverse1Id`, `l`.`OneToOne_Optional_Self1Id`, `l0`.`Id`, `l0`.`Date`, `l0`.`Level1_Optional_Id`, `l0`.`Level1_Required_Id`, `l0`.`Name`, `l0`.`OneToMany_Optional_Inverse2Id`, `l0`.`OneToMany_Optional_Self_Inverse2Id`, `l0`.`OneToMany_Required_Inverse2Id`, `l0`.`OneToMany_Required_Self_Inverse2Id`, `l0`.`OneToOne_Optional_PK_Inverse2Id`, `l0`.`OneToOne_Optional_Self2Id` +""" +SELECT `l`.`Id`, `l`.`Date`, `l`.`Name`, `l`.`OneToMany_Optional_Self_Inverse1Id`, `l`.`OneToMany_Required_Self_Inverse1Id`, `l`.`OneToOne_Optional_Self1Id`, `l0`.`Id`, `l0`.`Date`, `l0`.`Level1_Optional_Id`, `l0`.`Level1_Required_Id`, `l0`.`Name`, `l0`.`OneToMany_Optional_Inverse2Id`, `l0`.`OneToMany_Optional_Self_Inverse2Id`, `l0`.`OneToMany_Required_Inverse2Id`, `l0`.`OneToMany_Required_Self_Inverse2Id`, `l0`.`OneToOne_Optional_PK_Inverse2Id`, `l0`.`OneToOne_Optional_Self2Id` FROM `LevelOne` AS `l` LEFT JOIN `LevelTwo` AS `l0` ON `l`.`Id` = `l0`.`OneToMany_Optional_Inverse2Id` ORDER BY CASE - WHEN `l`.`Name` IS NOT NULL AND (`l`.`Name` LIKE '%03') THEN 1 + WHEN `l`.`Name` LIKE '%03' THEN 1 ELSE 2 -END, `l`.`Id`"); +END, `l`.`Id` +"""); } public override async Task Multiple_complex_include_select(bool async) @@ -819,8 +822,32 @@ public override async Task LeftJoin_with_Any_on_outer_source_and_projecting_coll { await base.LeftJoin_with_Any_on_outer_source_and_projecting_collection_from_inner(async); - AssertSql( - @"SELECT CASE + if (SingleStoreTestHelpers.HasPrimitiveCollectionsSupport(Fixture)) + { + AssertSql( +""" +SELECT CASE + WHEN `l0`.`Id` IS NULL THEN 0 + ELSE `l0`.`Id` +END, `l`.`Id`, `l0`.`Id`, `l1`.`Id`, `l1`.`Level2_Optional_Id`, `l1`.`Level2_Required_Id`, `l1`.`Name`, `l1`.`OneToMany_Optional_Inverse3Id`, `l1`.`OneToMany_Optional_Self_Inverse3Id`, `l1`.`OneToMany_Required_Inverse3Id`, `l1`.`OneToMany_Required_Self_Inverse3Id`, `l1`.`OneToOne_Optional_PK_Inverse3Id`, `l1`.`OneToOne_Optional_Self3Id` +FROM `LevelOne` AS `l` +LEFT JOIN `LevelTwo` AS `l0` ON `l`.`Id` = `l0`.`Level1_Required_Id` +LEFT JOIN `LevelThree` AS `l1` ON `l0`.`Id` = `l1`.`OneToMany_Required_Inverse3Id` +WHERE EXISTS ( + SELECT 1 + FROM JSON_TABLE('["L1 01","L1 02"]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` longtext PATH '$[0]' + )) AS `v` + WHERE (`v`.`value` = `l`.`Name`) OR (`v`.`value` IS NULL AND (`l`.`Name` IS NULL))) +ORDER BY `l`.`Id`, `l0`.`Id` +"""); + } + else + { + AssertSql( +""" +SELECT CASE WHEN `l0`.`Id` IS NULL THEN 0 ELSE `l0`.`Id` END, `l`.`Id`, `l0`.`Id`, `l1`.`Id`, `l1`.`Level2_Optional_Id`, `l1`.`Level2_Required_Id`, `l1`.`Name`, `l1`.`OneToMany_Optional_Inverse3Id`, `l1`.`OneToMany_Optional_Self_Inverse3Id`, `l1`.`OneToMany_Required_Inverse3Id`, `l1`.`OneToMany_Required_Self_Inverse3Id`, `l1`.`OneToOne_Optional_PK_Inverse3Id`, `l1`.`OneToOne_Optional_Self3Id` @@ -828,7 +855,9 @@ public override async Task LeftJoin_with_Any_on_outer_source_and_projecting_coll LEFT JOIN `LevelTwo` AS `l0` ON `l`.`Id` = `l0`.`Level1_Required_Id` LEFT JOIN `LevelThree` AS `l1` ON `l0`.`Id` = `l1`.`OneToMany_Required_Inverse3Id` WHERE `l`.`Name` IN ('L1 01', 'L1 02') -ORDER BY `l`.`Id`, `l0`.`Id`"); +ORDER BY `l`.`Id`, `l0`.`Id` +"""); + } } public override async Task Select_subquery_single_nested_subquery(bool async) @@ -1559,7 +1588,8 @@ public override async Task Filtered_include_Skip_Take_with_another_Skip_Take_on_ await base.Filtered_include_Skip_Take_with_another_Skip_Take_on_top_level(async); AssertSql( - @"@__p_1='5' +""" +@__p_1='5' @__p_0='10' SELECT `t`.`Id`, `t`.`Date`, `t`.`Name`, `t`.`OneToMany_Optional_Self_Inverse1Id`, `t`.`OneToMany_Required_Self_Inverse1Id`, `t`.`OneToOne_Optional_Self1Id`, `t0`.`Id`, `t0`.`Date`, `t0`.`Level1_Optional_Id`, `t0`.`Level1_Required_Id`, `t0`.`Name`, `t0`.`OneToMany_Optional_Inverse2Id`, `t0`.`OneToMany_Optional_Self_Inverse2Id`, `t0`.`OneToMany_Required_Inverse2Id`, `t0`.`OneToMany_Required_Self_Inverse2Id`, `t0`.`OneToOne_Optional_PK_Inverse2Id`, `t0`.`OneToOne_Optional_Self2Id`, `t0`.`Id0`, `t0`.`Level2_Optional_Id`, `t0`.`Level2_Required_Id`, `t0`.`Name0`, `t0`.`OneToMany_Optional_Inverse3Id`, `t0`.`OneToMany_Optional_Self_Inverse3Id`, `t0`.`OneToMany_Required_Inverse3Id`, `t0`.`OneToMany_Required_Self_Inverse3Id`, `t0`.`OneToOne_Optional_PK_Inverse3Id`, `t0`.`OneToOne_Optional_Self3Id` @@ -1576,11 +1606,12 @@ LEFT JOIN LATERAL ( FROM `LevelTwo` AS `l1` WHERE `t`.`Id` = `l1`.`OneToMany_Optional_Inverse2Id` ORDER BY `l1`.`Name` DESC - LIMIT 4 OFFSET 2 + LIMIT 4 OFFSET 1 ) AS `t1` LEFT JOIN `LevelThree` AS `l0` ON `t1`.`Id` = `l0`.`Level2_Optional_Id` ) AS `t0` ON TRUE -ORDER BY `t`.`Id` DESC, `t0`.`Name` DESC, `t0`.`Id`"); +ORDER BY `t`.`Id` DESC, `t0`.`Name` DESC, `t0`.`Id` +"""); } public override async Task Skip_Take_on_grouping_element_inside_collection_projection(bool async) @@ -1588,7 +1619,8 @@ public override async Task Skip_Take_on_grouping_element_inside_collection_proje await base.Skip_Take_on_grouping_element_inside_collection_projection(async); AssertSql( - @"SELECT `l`.`Id`, `t2`.`Date`, `t2`.`Id`, `t2`.`Date0`, `t2`.`Name`, `t2`.`OneToMany_Optional_Self_Inverse1Id`, `t2`.`OneToMany_Required_Self_Inverse1Id`, `t2`.`OneToOne_Optional_Self1Id` +""" +SELECT `l`.`Id`, `t2`.`Date`, `t2`.`Id`, `t2`.`Date0`, `t2`.`Name`, `t2`.`OneToMany_Optional_Self_Inverse1Id`, `t2`.`OneToMany_Required_Self_Inverse1Id`, `t2`.`OneToOne_Optional_Self1Id` FROM `LevelOne` AS `l` LEFT JOIN LATERAL ( SELECT `t`.`Date`, `t0`.`Id`, `t0`.`Date` AS `Date0`, `t0`.`Name`, `t0`.`OneToMany_Optional_Self_Inverse1Id`, `t0`.`OneToMany_Required_Self_Inverse1Id`, `t0`.`OneToOne_Optional_Self1Id` @@ -1608,7 +1640,8 @@ LEFT JOIN ( WHERE (1 < `t1`.`row`) AND (`t1`.`row` <= 6) ) AS `t0` ON `t`.`Date` = `t0`.`Date` ) AS `t2` ON TRUE -ORDER BY `l`.`Id`, `t2`.`Date`, `t2`.`Date0`, `t2`.`Name`"); +ORDER BY `l`.`Id`, `t2`.`Date`, `t2`.`Date0`, `t2`.`Name` +"""); } public override async Task Skip_Take_on_grouping_element_with_collection_include(bool async) diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/ComplexNavigationsCollectionsSplitQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/ComplexNavigationsCollectionsSplitQuerySingleStoreTest.cs index 5712b0b62..e2119716c 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/ComplexNavigationsCollectionsSplitQuerySingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/ComplexNavigationsCollectionsSplitQuerySingleStoreTest.cs @@ -1,5 +1,7 @@ using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.Query; +using EntityFrameworkCore.SingleStore.FunctionalTests.TestUtilities; +using EntityFrameworkCore.SingleStore.Tests; using Xunit; using Xunit.Abstractions; @@ -1492,20 +1494,24 @@ public override async Task Include_collection_with_conditional_order_by(bool asy await base.Include_collection_with_conditional_order_by(async); AssertSql( - @"SELECT `l`.`Id`, `l`.`Date`, `l`.`Name`, `l`.`OneToMany_Optional_Self_Inverse1Id`, `l`.`OneToMany_Required_Self_Inverse1Id`, `l`.`OneToOne_Optional_Self1Id` +""" +SELECT `l`.`Id`, `l`.`Date`, `l`.`Name`, `l`.`OneToMany_Optional_Self_Inverse1Id`, `l`.`OneToMany_Required_Self_Inverse1Id`, `l`.`OneToOne_Optional_Self1Id` FROM `LevelOne` AS `l` ORDER BY CASE - WHEN `l`.`Name` IS NOT NULL AND (`l`.`Name` LIKE '%03') THEN 1 + WHEN `l`.`Name` LIKE '%03' THEN 1 ELSE 2 -END, `l`.`Id`", - // - @"SELECT `l0`.`Id`, `l0`.`Date`, `l0`.`Level1_Optional_Id`, `l0`.`Level1_Required_Id`, `l0`.`Name`, `l0`.`OneToMany_Optional_Inverse2Id`, `l0`.`OneToMany_Optional_Self_Inverse2Id`, `l0`.`OneToMany_Required_Inverse2Id`, `l0`.`OneToMany_Required_Self_Inverse2Id`, `l0`.`OneToOne_Optional_PK_Inverse2Id`, `l0`.`OneToOne_Optional_Self2Id`, `l`.`Id` +END, `l`.`Id` +""", +// +""" +SELECT `l0`.`Id`, `l0`.`Date`, `l0`.`Level1_Optional_Id`, `l0`.`Level1_Required_Id`, `l0`.`Name`, `l0`.`OneToMany_Optional_Inverse2Id`, `l0`.`OneToMany_Optional_Self_Inverse2Id`, `l0`.`OneToMany_Required_Inverse2Id`, `l0`.`OneToMany_Required_Self_Inverse2Id`, `l0`.`OneToOne_Optional_PK_Inverse2Id`, `l0`.`OneToOne_Optional_Self2Id`, `l`.`Id` FROM `LevelOne` AS `l` INNER JOIN `LevelTwo` AS `l0` ON `l`.`Id` = `l0`.`OneToMany_Optional_Inverse2Id` ORDER BY CASE - WHEN `l`.`Name` IS NOT NULL AND (`l`.`Name` LIKE '%03') THEN 1 + WHEN `l`.`Name` LIKE '%03' THEN 1 ELSE 2 -END, `l`.`Id`"); +END, `l`.`Id` +"""); } public override async Task Multiple_complex_include_select(bool async) @@ -2299,22 +2305,64 @@ public override async Task LeftJoin_with_Any_on_outer_source_and_projecting_coll { await base.LeftJoin_with_Any_on_outer_source_and_projecting_collection_from_inner(async); - AssertSql( - @"SELECT CASE + if (SingleStoreTestHelpers.HasPrimitiveCollectionsSupport(Fixture)) + { + AssertSql( +""" +SELECT CASE + WHEN `l0`.`Id` IS NULL THEN 0 + ELSE `l0`.`Id` +END, `l`.`Id`, `l0`.`Id` +FROM `LevelOne` AS `l` +LEFT JOIN `LevelTwo` AS `l0` ON `l`.`Id` = `l0`.`Level1_Required_Id` +WHERE EXISTS ( + SELECT 1 + FROM JSON_TABLE('["L1 01","L1 02"]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` longtext PATH '$[0]' + )) AS `v` + WHERE (`v`.`value` = `l`.`Name`) OR (`v`.`value` IS NULL AND (`l`.`Name` IS NULL))) +ORDER BY `l`.`Id`, `l0`.`Id` +""", + // + """ +SELECT `l1`.`Id`, `l1`.`Level2_Optional_Id`, `l1`.`Level2_Required_Id`, `l1`.`Name`, `l1`.`OneToMany_Optional_Inverse3Id`, `l1`.`OneToMany_Optional_Self_Inverse3Id`, `l1`.`OneToMany_Required_Inverse3Id`, `l1`.`OneToMany_Required_Self_Inverse3Id`, `l1`.`OneToOne_Optional_PK_Inverse3Id`, `l1`.`OneToOne_Optional_Self3Id`, `l`.`Id`, `l0`.`Id` +FROM `LevelOne` AS `l` +LEFT JOIN `LevelTwo` AS `l0` ON `l`.`Id` = `l0`.`Level1_Required_Id` +INNER JOIN `LevelThree` AS `l1` ON `l0`.`Id` = `l1`.`OneToMany_Required_Inverse3Id` +WHERE EXISTS ( + SELECT 1 + FROM JSON_TABLE('["L1 01","L1 02"]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` longtext PATH '$[0]' + )) AS `v` + WHERE (`v`.`value` = `l`.`Name`) OR (`v`.`value` IS NULL AND (`l`.`Name` IS NULL))) +ORDER BY `l`.`Id`, `l0`.`Id` +"""); + } + else + { + AssertSql( +""" +SELECT CASE WHEN `l0`.`Id` IS NULL THEN 0 ELSE `l0`.`Id` END, `l`.`Id`, `l0`.`Id` FROM `LevelOne` AS `l` LEFT JOIN `LevelTwo` AS `l0` ON `l`.`Id` = `l0`.`Level1_Required_Id` WHERE `l`.`Name` IN ('L1 01', 'L1 02') -ORDER BY `l`.`Id`, `l0`.`Id`", +ORDER BY `l`.`Id`, `l0`.`Id` +""", // - @"SELECT `l1`.`Id`, `l1`.`Level2_Optional_Id`, `l1`.`Level2_Required_Id`, `l1`.`Name`, `l1`.`OneToMany_Optional_Inverse3Id`, `l1`.`OneToMany_Optional_Self_Inverse3Id`, `l1`.`OneToMany_Required_Inverse3Id`, `l1`.`OneToMany_Required_Self_Inverse3Id`, `l1`.`OneToOne_Optional_PK_Inverse3Id`, `l1`.`OneToOne_Optional_Self3Id`, `l`.`Id`, `l0`.`Id` + """ +SELECT `l1`.`Id`, `l1`.`Level2_Optional_Id`, `l1`.`Level2_Required_Id`, `l1`.`Name`, `l1`.`OneToMany_Optional_Inverse3Id`, `l1`.`OneToMany_Optional_Self_Inverse3Id`, `l1`.`OneToMany_Required_Inverse3Id`, `l1`.`OneToMany_Required_Self_Inverse3Id`, `l1`.`OneToOne_Optional_PK_Inverse3Id`, `l1`.`OneToOne_Optional_Self3Id`, `l`.`Id`, `l0`.`Id` FROM `LevelOne` AS `l` LEFT JOIN `LevelTwo` AS `l0` ON `l`.`Id` = `l0`.`Level1_Required_Id` INNER JOIN `LevelThree` AS `l1` ON `l0`.`Id` = `l1`.`OneToMany_Required_Inverse3Id` WHERE `l`.`Name` IN ('L1 01', 'L1 02') -ORDER BY `l`.`Id`, `l0`.`Id`"); +ORDER BY `l`.`Id`, `l0`.`Id` +"""); + } } public override async Task Filtered_include_different_filter_set_on_same_navigation_twice(bool async) @@ -2415,15 +2463,18 @@ public override async Task Filtered_include_Skip_Take_with_another_Skip_Take_on_ await base.Filtered_include_Skip_Take_with_another_Skip_Take_on_top_level(async); AssertSql( - @"@__p_1='5' +""" +@__p_1='5' @__p_0='10' SELECT `l`.`Id`, `l`.`Date`, `l`.`Name`, `l`.`OneToMany_Optional_Self_Inverse1Id`, `l`.`OneToMany_Required_Self_Inverse1Id`, `l`.`OneToOne_Optional_Self1Id` FROM `LevelOne` AS `l` ORDER BY `l`.`Id` DESC -LIMIT @__p_1 OFFSET @__p_0", +LIMIT @__p_1 OFFSET @__p_0 +""", // - @"@__p_1='5' +""" +@__p_1='5' @__p_0='10' SELECT `t0`.`Id`, `t0`.`Date`, `t0`.`Level1_Optional_Id`, `t0`.`Level1_Required_Id`, `t0`.`Name`, `t0`.`OneToMany_Optional_Inverse2Id`, `t0`.`OneToMany_Optional_Self_Inverse2Id`, `t0`.`OneToMany_Required_Inverse2Id`, `t0`.`OneToMany_Required_Self_Inverse2Id`, `t0`.`OneToOne_Optional_PK_Inverse2Id`, `t0`.`OneToOne_Optional_Self2Id`, `t0`.`Id0`, `t0`.`Level2_Optional_Id`, `t0`.`Level2_Required_Id`, `t0`.`Name0`, `t0`.`OneToMany_Optional_Inverse3Id`, `t0`.`OneToMany_Optional_Self_Inverse3Id`, `t0`.`OneToMany_Required_Inverse3Id`, `t0`.`OneToMany_Required_Self_Inverse3Id`, `t0`.`OneToOne_Optional_PK_Inverse3Id`, `t0`.`OneToOne_Optional_Self3Id`, `t`.`Id` @@ -2440,11 +2491,12 @@ JOIN LATERAL ( FROM `LevelTwo` AS `l1` WHERE `t`.`Id` = `l1`.`OneToMany_Optional_Inverse2Id` ORDER BY `l1`.`Name` DESC - LIMIT 4 OFFSET 2 + LIMIT 4 OFFSET 1 ) AS `t1` LEFT JOIN `LevelThree` AS `l0` ON `t1`.`Id` = `l0`.`Level2_Optional_Id` ) AS `t0` ON TRUE -ORDER BY `t`.`Id` DESC, `t0`.`Name` DESC"); +ORDER BY `t`.`Id` DESC, `t0`.`Name` DESC +"""); } public override async Task Skip_Take_Distinct_on_grouping_element(bool async) diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/ComplexNavigationsQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/ComplexNavigationsQuerySingleStoreTest.cs index 5fa01a23d..95e97479f 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/ComplexNavigationsQuerySingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/ComplexNavigationsQuerySingleStoreTest.cs @@ -5,7 +5,6 @@ using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.TestModels.ComplexNavigationsModel; using EntityFrameworkCore.SingleStore.Infrastructure; -using EntityFrameworkCore.SingleStore.Tests; using EntityFrameworkCore.SingleStore.Tests.TestUtilities.Attributes; using Xunit; using Xunit.Abstractions; @@ -25,19 +24,19 @@ public ComplexNavigationsQuerySingleStoreTest(ComplexNavigationsQuerySingleStore protected override bool CanExecuteQueryString => true; - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override Task Collection_FirstOrDefault_property_accesses_in_projection(bool async) { return base.Collection_FirstOrDefault_property_accesses_in_projection(async); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override Task Contains_over_optional_navigation_with_null_column(bool async) { return base.Contains_over_optional_navigation_with_null_column(async); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override Task Contains_over_optional_navigation_with_null_entity_reference(bool async) { return base.Contains_over_optional_navigation_with_null_entity_reference(async); @@ -91,7 +90,7 @@ public override Task OrderBy_collection_count_ThenBy_reference_navigation(bool a return base.OrderBy_collection_count_ThenBy_reference_navigation(async); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override Task Project_collection_navigation_count(bool async) { return base.Project_collection_navigation_count(async); @@ -103,7 +102,7 @@ public override Task Subquery_with_Distinct_Skip_FirstOrDefault_without_OrderBy( return base.Subquery_with_Distinct_Skip_FirstOrDefault_without_OrderBy(async); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override Task Where_navigation_property_to_collection(bool async) { return base.Where_navigation_property_to_collection(async); @@ -177,6 +176,19 @@ JOIN LATERAL ( ) AS `t0` ON TRUE"); } + public override async Task Method_call_on_optional_navigation_translates_to_null_conditional_properly_for_arguments(bool async) + { + await base.Method_call_on_optional_navigation_translates_to_null_conditional_properly_for_arguments(async); + + AssertSql( + """ + SELECT `l`.`Id`, `l`.`Date`, `l`.`Name`, `l`.`OneToMany_Optional_Self_Inverse1Id`, `l`.`OneToMany_Required_Self_Inverse1Id`, `l`.`OneToOne_Optional_Self1Id` + FROM `LevelOne` AS `l` + LEFT JOIN `LevelTwo` AS `l0` ON `l`.`Id` = `l0`.`Level1_Optional_Id` + WHERE `l0`.`Name` IS NOT NULL AND (LEFT(`l0`.`Name`, CHAR_LENGTH(`l0`.`Name`)) = `l0`.`Name`) + """); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySingleStoreTest.cs index 5f06bc792..482421771 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySingleStoreTest.cs @@ -27,7 +27,7 @@ public ComplexNavigationsSharedTypeQuerySingleStoreTest( // Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } - [ConditionalTheory(Skip = "Feature 'Scalar subselect where outer table is not a sharded table' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Scalar subselect where outer table is not a sharded table' is not supported by SingleStore Distributed")] public override Task Composite_key_join_on_groupby_aggregate_projecting_only_grouping_key(bool async) { return base.Composite_key_join_on_groupby_aggregate_projecting_only_grouping_key(async); @@ -65,19 +65,19 @@ public override Task Element_selector_with_coalesce_repeated_in_aggregate(bool a return base.Element_selector_with_coalesce_repeated_in_aggregate(async); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override Task Collection_FirstOrDefault_property_accesses_in_projection(bool async) { return base.Collection_FirstOrDefault_property_accesses_in_projection(async); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override Task Contains_over_optional_navigation_with_null_column(bool async) { return base.Contains_over_optional_navigation_with_null_column(async); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override Task Contains_over_optional_navigation_with_null_entity_reference(bool async) { return base.Contains_over_optional_navigation_with_null_entity_reference(async); @@ -184,6 +184,23 @@ await Assert.ThrowsAsync( AssertSql(); } + public override async Task Method_call_on_optional_navigation_translates_to_null_conditional_properly_for_arguments(bool async) + { + await base.Method_call_on_optional_navigation_translates_to_null_conditional_properly_for_arguments(async); + + AssertSql( + """ + SELECT `l`.`Id`, `l`.`Date`, `l`.`Name` + FROM `Level1` AS `l` + LEFT JOIN ( + SELECT `l0`.`Level1_Optional_Id`, `l0`.`Level2_Name` + FROM `Level1` AS `l0` + WHERE (`l0`.`OneToOne_Required_PK_Date` IS NOT NULL AND (`l0`.`Level1_Required_Id` IS NOT NULL)) AND `l0`.`OneToMany_Required_Inverse2Id` IS NOT NULL + ) AS `t` ON `l`.`Id` = `t`.`Level1_Optional_Id` + WHERE `t`.`Level2_Name` IS NOT NULL AND (LEFT(`t`.`Level2_Name`, CHAR_LENGTH(`t`.`Level2_Name`)) = `t`.`Level2_Name`) + """); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/ComplexTypeQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/ComplexTypeQuerySingleStoreTest.cs new file mode 100644 index 000000000..0a80bef5e --- /dev/null +++ b/test/EFCore.SingleStore.FunctionalTests/Query/ComplexTypeQuerySingleStoreTest.cs @@ -0,0 +1,751 @@ +using System; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.TestUtilities; +using EntityFrameworkCore.SingleStore.FunctionalTests.TestUtilities; +using Xunit; +using Xunit.Abstractions; + +namespace EntityFrameworkCore.SingleStore.FunctionalTests.Query; + +public class ComplexTypeQuerySingleStoreTest : ComplexTypeQueryRelationalTestBase< + ComplexTypeQuerySingleStoreTest.ComplexTypeQuerySingleStoreFixture> +{ + public ComplexTypeQuerySingleStoreTest(ComplexTypeQuerySingleStoreFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task Filter_on_property_inside_complex_type(bool async) + { + await base.Filter_on_property_inside_complex_type(async); + + AssertSql( +""" +SELECT `c`.`Id`, `c`.`Name`, `c`.`BillingAddress_AddressLine1`, `c`.`BillingAddress_AddressLine2`, `c`.`BillingAddress_ZipCode`, `c`.`BillingAddress_Country_Code`, `c`.`BillingAddress_Country_FullName`, `c`.`ShippingAddress_AddressLine1`, `c`.`ShippingAddress_AddressLine2`, `c`.`ShippingAddress_ZipCode`, `c`.`ShippingAddress_Country_Code`, `c`.`ShippingAddress_Country_FullName` +FROM `Customer` AS `c` +WHERE `c`.`ShippingAddress_ZipCode` = 7728 +"""); + } + + public override async Task Filter_on_property_inside_nested_complex_type(bool async) + { + await base.Filter_on_property_inside_nested_complex_type(async); + + AssertSql( +""" +SELECT `c`.`Id`, `c`.`Name`, `c`.`BillingAddress_AddressLine1`, `c`.`BillingAddress_AddressLine2`, `c`.`BillingAddress_ZipCode`, `c`.`BillingAddress_Country_Code`, `c`.`BillingAddress_Country_FullName`, `c`.`ShippingAddress_AddressLine1`, `c`.`ShippingAddress_AddressLine2`, `c`.`ShippingAddress_ZipCode`, `c`.`ShippingAddress_Country_Code`, `c`.`ShippingAddress_Country_FullName` +FROM `Customer` AS `c` +WHERE `c`.`ShippingAddress_Country_Code` = 'DE' +"""); + } + + public override async Task Filter_on_property_inside_complex_type_after_subquery(bool async) + { + await base.Filter_on_property_inside_complex_type_after_subquery(async); + + AssertSql( +""" +@__p_0='1' +SELECT DISTINCT `t`.`Id`, `t`.`Name`, `t`.`BillingAddress_AddressLine1`, `t`.`BillingAddress_AddressLine2`, `t`.`BillingAddress_ZipCode`, `t`.`BillingAddress_Country_Code`, `t`.`BillingAddress_Country_FullName`, `t`.`ShippingAddress_AddressLine1`, `t`.`ShippingAddress_AddressLine2`, `t`.`ShippingAddress_ZipCode`, `t`.`ShippingAddress_Country_Code`, `t`.`ShippingAddress_Country_FullName` +FROM ( + SELECT `c`.`Id`, `c`.`Name`, `c`.`BillingAddress_AddressLine1`, `c`.`BillingAddress_AddressLine2`, `c`.`BillingAddress_ZipCode`, `c`.`BillingAddress_Country_Code`, `c`.`BillingAddress_Country_FullName`, `c`.`ShippingAddress_AddressLine1`, `c`.`ShippingAddress_AddressLine2`, `c`.`ShippingAddress_ZipCode`, `c`.`ShippingAddress_Country_Code`, `c`.`ShippingAddress_Country_FullName` + FROM `Customer` AS `c` + ORDER BY `c`.`Id` + LIMIT 18446744073709551610 OFFSET @__p_0 +) AS `t` +WHERE `t`.`ShippingAddress_ZipCode` = 7728 +"""); + } + + public override async Task Filter_on_property_inside_nested_complex_type_after_subquery(bool async) + { + await base.Filter_on_property_inside_nested_complex_type_after_subquery(async); + + AssertSql( +""" +@__p_0='1' +SELECT DISTINCT `t`.`Id`, `t`.`Name`, `t`.`BillingAddress_AddressLine1`, `t`.`BillingAddress_AddressLine2`, `t`.`BillingAddress_ZipCode`, `t`.`BillingAddress_Country_Code`, `t`.`BillingAddress_Country_FullName`, `t`.`ShippingAddress_AddressLine1`, `t`.`ShippingAddress_AddressLine2`, `t`.`ShippingAddress_ZipCode`, `t`.`ShippingAddress_Country_Code`, `t`.`ShippingAddress_Country_FullName` +FROM ( + SELECT `c`.`Id`, `c`.`Name`, `c`.`BillingAddress_AddressLine1`, `c`.`BillingAddress_AddressLine2`, `c`.`BillingAddress_ZipCode`, `c`.`BillingAddress_Country_Code`, `c`.`BillingAddress_Country_FullName`, `c`.`ShippingAddress_AddressLine1`, `c`.`ShippingAddress_AddressLine2`, `c`.`ShippingAddress_ZipCode`, `c`.`ShippingAddress_Country_Code`, `c`.`ShippingAddress_Country_FullName` + FROM `Customer` AS `c` + ORDER BY `c`.`Id` + LIMIT 18446744073709551610 OFFSET @__p_0 +) AS `t` +WHERE `t`.`ShippingAddress_Country_Code` = 'DE' +"""); + } + + public override async Task Filter_on_required_property_inside_required_complex_type_on_optional_navigation(bool async) + { + await base.Filter_on_required_property_inside_required_complex_type_on_optional_navigation(async); + + AssertSql( +""" +SELECT `c`.`Id`, `c`.`OptionalCustomerId`, `c`.`RequiredCustomerId`, `c0`.`Id`, `c0`.`Name`, `c0`.`BillingAddress_AddressLine1`, `c0`.`BillingAddress_AddressLine2`, `c0`.`BillingAddress_ZipCode`, `c0`.`BillingAddress_Country_Code`, `c0`.`BillingAddress_Country_FullName`, `c0`.`ShippingAddress_AddressLine1`, `c0`.`ShippingAddress_AddressLine2`, `c0`.`ShippingAddress_ZipCode`, `c0`.`ShippingAddress_Country_Code`, `c0`.`ShippingAddress_Country_FullName`, `c1`.`Id`, `c1`.`Name`, `c1`.`BillingAddress_AddressLine1`, `c1`.`BillingAddress_AddressLine2`, `c1`.`BillingAddress_ZipCode`, `c1`.`BillingAddress_Country_Code`, `c1`.`BillingAddress_Country_FullName`, `c1`.`ShippingAddress_AddressLine1`, `c1`.`ShippingAddress_AddressLine2`, `c1`.`ShippingAddress_ZipCode`, `c1`.`ShippingAddress_Country_Code`, `c1`.`ShippingAddress_Country_FullName` +FROM `CustomerGroup` AS `c` +LEFT JOIN `Customer` AS `c0` ON `c`.`OptionalCustomerId` = `c0`.`Id` +INNER JOIN `Customer` AS `c1` ON `c`.`RequiredCustomerId` = `c1`.`Id` +WHERE (`c0`.`ShippingAddress_ZipCode` <> 7728) OR `c0`.`ShippingAddress_ZipCode` IS NULL +"""); + } + + public override async Task Filter_on_required_property_inside_required_complex_type_on_required_navigation(bool async) + { + await base.Filter_on_required_property_inside_required_complex_type_on_required_navigation(async); + + AssertSql( +""" +SELECT `c`.`Id`, `c`.`OptionalCustomerId`, `c`.`RequiredCustomerId`, `c1`.`Id`, `c1`.`Name`, `c1`.`BillingAddress_AddressLine1`, `c1`.`BillingAddress_AddressLine2`, `c1`.`BillingAddress_ZipCode`, `c1`.`BillingAddress_Country_Code`, `c1`.`BillingAddress_Country_FullName`, `c1`.`ShippingAddress_AddressLine1`, `c1`.`ShippingAddress_AddressLine2`, `c1`.`ShippingAddress_ZipCode`, `c1`.`ShippingAddress_Country_Code`, `c1`.`ShippingAddress_Country_FullName`, `c0`.`Id`, `c0`.`Name`, `c0`.`BillingAddress_AddressLine1`, `c0`.`BillingAddress_AddressLine2`, `c0`.`BillingAddress_ZipCode`, `c0`.`BillingAddress_Country_Code`, `c0`.`BillingAddress_Country_FullName`, `c0`.`ShippingAddress_AddressLine1`, `c0`.`ShippingAddress_AddressLine2`, `c0`.`ShippingAddress_ZipCode`, `c0`.`ShippingAddress_Country_Code`, `c0`.`ShippingAddress_Country_FullName` +FROM `CustomerGroup` AS `c` +INNER JOIN `Customer` AS `c0` ON `c`.`RequiredCustomerId` = `c0`.`Id` +LEFT JOIN `Customer` AS `c1` ON `c`.`OptionalCustomerId` = `c1`.`Id` +WHERE `c0`.`ShippingAddress_ZipCode` <> 7728 +"""); + } + + // This test fails because when OptionalCustomer is null, we get all-null results because of the LEFT JOIN, and we materialize this + // as an empty ShippingAddress instead of null (see SQL). The proper solution here would be to project the Customer ID just for the + // purpose of knowing that it's there. + public override async Task Project_complex_type_via_optional_navigation(bool async) + { + var exception = await Assert.ThrowsAsync(() => base.Project_complex_type_via_optional_navigation(async)); + + Assert.Equal(RelationalStrings.CannotProjectNullableComplexType("Customer.ShippingAddress#Address"), exception.Message); + } + + public override async Task Project_complex_type_via_required_navigation(bool async) + { + await base.Project_complex_type_via_required_navigation(async); + + AssertSql( +""" +SELECT `c0`.`ShippingAddress_AddressLine1`, `c0`.`ShippingAddress_AddressLine2`, `c0`.`ShippingAddress_ZipCode`, `c0`.`ShippingAddress_Country_Code`, `c0`.`ShippingAddress_Country_FullName` +FROM `CustomerGroup` AS `c` +INNER JOIN `Customer` AS `c0` ON `c`.`RequiredCustomerId` = `c0`.`Id` +"""); + } + + public override async Task Load_complex_type_after_subquery_on_entity_type(bool async) + { + await base.Load_complex_type_after_subquery_on_entity_type(async); + + AssertSql( +""" +@__p_0='1' +SELECT DISTINCT `t`.`Id`, `t`.`Name`, `t`.`BillingAddress_AddressLine1`, `t`.`BillingAddress_AddressLine2`, `t`.`BillingAddress_ZipCode`, `t`.`BillingAddress_Country_Code`, `t`.`BillingAddress_Country_FullName`, `t`.`ShippingAddress_AddressLine1`, `t`.`ShippingAddress_AddressLine2`, `t`.`ShippingAddress_ZipCode`, `t`.`ShippingAddress_Country_Code`, `t`.`ShippingAddress_Country_FullName` +FROM ( + SELECT `c`.`Id`, `c`.`Name`, `c`.`BillingAddress_AddressLine1`, `c`.`BillingAddress_AddressLine2`, `c`.`BillingAddress_ZipCode`, `c`.`BillingAddress_Country_Code`, `c`.`BillingAddress_Country_FullName`, `c`.`ShippingAddress_AddressLine1`, `c`.`ShippingAddress_AddressLine2`, `c`.`ShippingAddress_ZipCode`, `c`.`ShippingAddress_Country_Code`, `c`.`ShippingAddress_Country_FullName` + FROM `Customer` AS `c` + ORDER BY `c`.`Id` + LIMIT 18446744073709551610 OFFSET @__p_0 +) AS `t` +"""); + } + + public override async Task Select_complex_type(bool async) + { + await base.Select_complex_type(async); + + AssertSql( +""" +SELECT `c`.`ShippingAddress_AddressLine1`, `c`.`ShippingAddress_AddressLine2`, `c`.`ShippingAddress_ZipCode`, `c`.`ShippingAddress_Country_Code`, `c`.`ShippingAddress_Country_FullName` +FROM `Customer` AS `c` +"""); + } + + public override async Task Select_nested_complex_type(bool async) + { + await base.Select_nested_complex_type(async); + + AssertSql( +""" +SELECT `c`.`ShippingAddress_Country_Code`, `c`.`ShippingAddress_Country_FullName` +FROM `Customer` AS `c` +"""); + } + + public override async Task Select_single_property_on_nested_complex_type(bool async) + { + await base.Select_single_property_on_nested_complex_type(async); + + AssertSql( +""" +SELECT `c`.`ShippingAddress_Country_FullName` +FROM `Customer` AS `c` +"""); + } + + public override async Task Select_complex_type_Where(bool async) + { + await base.Select_complex_type_Where(async); + + AssertSql( +""" +SELECT `c`.`ShippingAddress_AddressLine1`, `c`.`ShippingAddress_AddressLine2`, `c`.`ShippingAddress_ZipCode`, `c`.`ShippingAddress_Country_Code`, `c`.`ShippingAddress_Country_FullName` +FROM `Customer` AS `c` +WHERE `c`.`ShippingAddress_ZipCode` = 7728 +"""); + } + + public override async Task Select_complex_type_Distinct(bool async) + { + await base.Select_complex_type_Distinct(async); + + AssertSql( +""" +SELECT DISTINCT `c`.`ShippingAddress_AddressLine1`, `c`.`ShippingAddress_AddressLine2`, `c`.`ShippingAddress_ZipCode`, `c`.`ShippingAddress_Country_Code`, `c`.`ShippingAddress_Country_FullName` +FROM `Customer` AS `c` +"""); + } + + public override async Task Complex_type_equals_complex_type(bool async) + { + await base.Complex_type_equals_complex_type(async); + + AssertSql( +""" +SELECT `c`.`Id`, `c`.`Name`, `c`.`BillingAddress_AddressLine1`, `c`.`BillingAddress_AddressLine2`, `c`.`BillingAddress_ZipCode`, `c`.`BillingAddress_Country_Code`, `c`.`BillingAddress_Country_FullName`, `c`.`ShippingAddress_AddressLine1`, `c`.`ShippingAddress_AddressLine2`, `c`.`ShippingAddress_ZipCode`, `c`.`ShippingAddress_Country_Code`, `c`.`ShippingAddress_Country_FullName` +FROM `Customer` AS `c` +WHERE ((`c`.`ShippingAddress_AddressLine1` = `c`.`BillingAddress_AddressLine1`) AND ((`c`.`ShippingAddress_AddressLine2` = `c`.`BillingAddress_AddressLine2`) OR (`c`.`ShippingAddress_AddressLine2` IS NULL AND (`c`.`BillingAddress_AddressLine2` IS NULL)))) AND (`c`.`ShippingAddress_ZipCode` = `c`.`BillingAddress_ZipCode`) +"""); + } + + public override async Task Complex_type_equals_constant(bool async) + { + await base.Complex_type_equals_constant(async); + + AssertSql( +""" +SELECT `c`.`Id`, `c`.`Name`, `c`.`BillingAddress_AddressLine1`, `c`.`BillingAddress_AddressLine2`, `c`.`BillingAddress_ZipCode`, `c`.`BillingAddress_Country_Code`, `c`.`BillingAddress_Country_FullName`, `c`.`ShippingAddress_AddressLine1`, `c`.`ShippingAddress_AddressLine2`, `c`.`ShippingAddress_ZipCode`, `c`.`ShippingAddress_Country_Code`, `c`.`ShippingAddress_Country_FullName` +FROM `Customer` AS `c` +WHERE ((((`c`.`ShippingAddress_AddressLine1` = '804 S. Lakeshore Road') AND `c`.`ShippingAddress_AddressLine2` IS NULL) AND (`c`.`ShippingAddress_ZipCode` = 38654)) AND (`c`.`ShippingAddress_Country_Code` = 'US')) AND (`c`.`ShippingAddress_Country_FullName` = 'United States') +"""); + } + + public override async Task Complex_type_equals_parameter(bool async) + { + await base.Complex_type_equals_parameter(async); + + AssertSql( +""" +@__entity_equality_address_0_AddressLine1='804 S. Lakeshore Road' (Size = 4000) +@__entity_equality_address_0_ZipCode='38654' (Nullable = true) +@__entity_equality_address_0_Code='US' (Size = 4000) +@__entity_equality_address_0_FullName='United States' (Size = 4000) +SELECT `c`.`Id`, `c`.`Name`, `c`.`BillingAddress_AddressLine1`, `c`.`BillingAddress_AddressLine2`, `c`.`BillingAddress_ZipCode`, `c`.`BillingAddress_Country_Code`, `c`.`BillingAddress_Country_FullName`, `c`.`ShippingAddress_AddressLine1`, `c`.`ShippingAddress_AddressLine2`, `c`.`ShippingAddress_ZipCode`, `c`.`ShippingAddress_Country_Code`, `c`.`ShippingAddress_Country_FullName` +FROM `Customer` AS `c` +WHERE ((((`c`.`ShippingAddress_AddressLine1` = @__entity_equality_address_0_AddressLine1) AND `c`.`ShippingAddress_AddressLine2` IS NULL) AND (`c`.`ShippingAddress_ZipCode` = @__entity_equality_address_0_ZipCode)) AND (`c`.`ShippingAddress_Country_Code` = @__entity_equality_address_0_Code)) AND (`c`.`ShippingAddress_Country_FullName` = @__entity_equality_address_0_FullName) +"""); + } + + public override async Task Subquery_over_complex_type(bool async) + { + await base.Subquery_over_complex_type(async); + + AssertSql( +); + } + + public override async Task Contains_over_complex_type(bool async) + { + await base.Contains_over_complex_type(async); + + AssertSql( +""" +@__entity_equality_address_0_AddressLine1='804 S. Lakeshore Road' (Size = 4000) +@__entity_equality_address_0_ZipCode='38654' (Nullable = true) +@__entity_equality_address_0_Code='US' (Size = 4000) +@__entity_equality_address_0_FullName='United States' (Size = 4000) +SELECT `c`.`Id`, `c`.`Name`, `c`.`BillingAddress_AddressLine1`, `c`.`BillingAddress_AddressLine2`, `c`.`BillingAddress_ZipCode`, `c`.`BillingAddress_Country_Code`, `c`.`BillingAddress_Country_FullName`, `c`.`ShippingAddress_AddressLine1`, `c`.`ShippingAddress_AddressLine2`, `c`.`ShippingAddress_ZipCode`, `c`.`ShippingAddress_Country_Code`, `c`.`ShippingAddress_Country_FullName` +FROM `Customer` AS `c` +WHERE EXISTS ( + SELECT 1 + FROM `Customer` AS `c0` + WHERE ((((`c0`.`ShippingAddress_AddressLine1` = @__entity_equality_address_0_AddressLine1) AND `c0`.`ShippingAddress_AddressLine2` IS NULL) AND (`c0`.`ShippingAddress_ZipCode` = @__entity_equality_address_0_ZipCode)) AND (`c0`.`ShippingAddress_Country_Code` = @__entity_equality_address_0_Code)) AND (`c0`.`ShippingAddress_Country_FullName` = @__entity_equality_address_0_FullName)) +"""); + } + + public override async Task Concat_complex_type(bool async) + { + await base.Concat_complex_type(async); + AssertSql( +""" +SELECT `c`.`ShippingAddress_AddressLine1`, `c`.`ShippingAddress_AddressLine2`, `c`.`ShippingAddress_ZipCode`, `c`.`ShippingAddress_Country_Code`, `c`.`ShippingAddress_Country_FullName` +FROM `Customer` AS `c` +WHERE `c`.`Id` = 1 +UNION ALL +SELECT `c0`.`ShippingAddress_AddressLine1`, `c0`.`ShippingAddress_AddressLine2`, `c0`.`ShippingAddress_ZipCode`, `c0`.`ShippingAddress_Country_Code`, `c0`.`ShippingAddress_Country_FullName` +FROM `Customer` AS `c0` +WHERE `c0`.`Id` = 2 +"""); + } + + public override async Task Concat_entity_type_containing_complex_property(bool async) + { + await base.Concat_entity_type_containing_complex_property(async); + + AssertSql( +""" +SELECT `c`.`Id`, `c`.`Name`, `c`.`BillingAddress_AddressLine1`, `c`.`BillingAddress_AddressLine2`, `c`.`BillingAddress_ZipCode`, `c`.`BillingAddress_Country_Code`, `c`.`BillingAddress_Country_FullName`, `c`.`ShippingAddress_AddressLine1`, `c`.`ShippingAddress_AddressLine2`, `c`.`ShippingAddress_ZipCode`, `c`.`ShippingAddress_Country_Code`, `c`.`ShippingAddress_Country_FullName` +FROM `Customer` AS `c` +WHERE `c`.`Id` = 1 +UNION ALL +SELECT `c0`.`Id`, `c0`.`Name`, `c0`.`BillingAddress_AddressLine1`, `c0`.`BillingAddress_AddressLine2`, `c0`.`BillingAddress_ZipCode`, `c0`.`BillingAddress_Country_Code`, `c0`.`BillingAddress_Country_FullName`, `c0`.`ShippingAddress_AddressLine1`, `c0`.`ShippingAddress_AddressLine2`, `c0`.`ShippingAddress_ZipCode`, `c0`.`ShippingAddress_Country_Code`, `c0`.`ShippingAddress_Country_FullName` +FROM `Customer` AS `c0` +WHERE `c0`.`Id` = 2 +"""); + } + + public override async Task Union_entity_type_containing_complex_property(bool async) + { + await base.Union_entity_type_containing_complex_property(async); + + AssertSql( +""" +SELECT `c`.`Id`, `c`.`Name`, `c`.`BillingAddress_AddressLine1`, `c`.`BillingAddress_AddressLine2`, `c`.`BillingAddress_ZipCode`, `c`.`BillingAddress_Country_Code`, `c`.`BillingAddress_Country_FullName`, `c`.`ShippingAddress_AddressLine1`, `c`.`ShippingAddress_AddressLine2`, `c`.`ShippingAddress_ZipCode`, `c`.`ShippingAddress_Country_Code`, `c`.`ShippingAddress_Country_FullName` +FROM `Customer` AS `c` +WHERE `c`.`Id` = 1 +UNION +SELECT `c0`.`Id`, `c0`.`Name`, `c0`.`BillingAddress_AddressLine1`, `c0`.`BillingAddress_AddressLine2`, `c0`.`BillingAddress_ZipCode`, `c0`.`BillingAddress_Country_Code`, `c0`.`BillingAddress_Country_FullName`, `c0`.`ShippingAddress_AddressLine1`, `c0`.`ShippingAddress_AddressLine2`, `c0`.`ShippingAddress_ZipCode`, `c0`.`ShippingAddress_Country_Code`, `c0`.`ShippingAddress_Country_FullName` +FROM `Customer` AS `c0` +WHERE `c0`.`Id` = 2 +"""); + } + + public override async Task Union_complex_type(bool async) + { + await base.Union_complex_type(async); + + AssertSql( +""" +SELECT `c`.`ShippingAddress_AddressLine1`, `c`.`ShippingAddress_AddressLine2`, `c`.`ShippingAddress_ZipCode`, `c`.`ShippingAddress_Country_Code`, `c`.`ShippingAddress_Country_FullName` +FROM `Customer` AS `c` +WHERE `c`.`Id` = 1 +UNION +SELECT `c0`.`ShippingAddress_AddressLine1`, `c0`.`ShippingAddress_AddressLine2`, `c0`.`ShippingAddress_ZipCode`, `c0`.`ShippingAddress_Country_Code`, `c0`.`ShippingAddress_Country_FullName` +FROM `Customer` AS `c0` +WHERE `c0`.`Id` = 2 +"""); + } + + public override async Task Concat_property_in_complex_type(bool async) + { + await base.Concat_property_in_complex_type(async); + + AssertSql( +""" +SELECT `c`.`ShippingAddress_AddressLine1` +FROM `Customer` AS `c` +UNION ALL +SELECT `c0`.`BillingAddress_AddressLine1` AS `ShippingAddress_AddressLine1` +FROM `Customer` AS `c0` +"""); + } + + public override async Task Union_property_in_complex_type(bool async) + { + await base.Union_property_in_complex_type(async); + + AssertSql( +""" +SELECT `c`.`ShippingAddress_AddressLine1` +FROM `Customer` AS `c` +UNION +SELECT `c0`.`BillingAddress_AddressLine1` AS `ShippingAddress_AddressLine1` +FROM `Customer` AS `c0` +"""); + } + + public override async Task Concat_two_different_complex_type(bool async) + { + await base.Concat_two_different_complex_type(async); + + AssertSql( +); + } + + public override async Task Union_two_different_complex_type(bool async) + { + await base.Union_two_different_complex_type(async); + + AssertSql( +); + } + + public override async Task Complex_type_equals_null(bool async) + { + await base.Complex_type_equals_null(async); + + AssertSql( +); + } + + public override async Task Subquery_over_struct_complex_type(bool async) + { + await base.Subquery_over_struct_complex_type(async); + + AssertSql( +); + } + + public override async Task Concat_two_different_struct_complex_type(bool async) + { + await base.Concat_two_different_struct_complex_type(async); + + AssertSql( +); + } + + public override async Task Union_two_different_struct_complex_type(bool async) + { + await base.Union_two_different_struct_complex_type(async); + + AssertSql( +); + } + + public override async Task Filter_on_property_inside_struct_complex_type(bool async) + { + await base.Filter_on_property_inside_struct_complex_type(async); + + AssertSql( +""" +SELECT `v`.`Id`, `v`.`Name`, `v`.`BillingAddress_AddressLine1`, `v`.`BillingAddress_AddressLine2`, `v`.`BillingAddress_ZipCode`, `v`.`BillingAddress_Country_Code`, `v`.`BillingAddress_Country_FullName`, `v`.`ShippingAddress_AddressLine1`, `v`.`ShippingAddress_AddressLine2`, `v`.`ShippingAddress_ZipCode`, `v`.`ShippingAddress_Country_Code`, `v`.`ShippingAddress_Country_FullName` +FROM `ValuedCustomer` AS `v` +WHERE `v`.`ShippingAddress_ZipCode` = 7728 +"""); + } + + public override async Task Filter_on_property_inside_nested_struct_complex_type(bool async) + { + await base.Filter_on_property_inside_nested_struct_complex_type(async); + + AssertSql( +""" +SELECT `v`.`Id`, `v`.`Name`, `v`.`BillingAddress_AddressLine1`, `v`.`BillingAddress_AddressLine2`, `v`.`BillingAddress_ZipCode`, `v`.`BillingAddress_Country_Code`, `v`.`BillingAddress_Country_FullName`, `v`.`ShippingAddress_AddressLine1`, `v`.`ShippingAddress_AddressLine2`, `v`.`ShippingAddress_ZipCode`, `v`.`ShippingAddress_Country_Code`, `v`.`ShippingAddress_Country_FullName` +FROM `ValuedCustomer` AS `v` +WHERE `v`.`ShippingAddress_Country_Code` = 'DE' +"""); + } + + public override async Task Filter_on_property_inside_struct_complex_type_after_subquery(bool async) + { + await base.Filter_on_property_inside_struct_complex_type_after_subquery(async); + + AssertSql( +""" +@__p_0='1' +SELECT DISTINCT `t`.`Id`, `t`.`Name`, `t`.`BillingAddress_AddressLine1`, `t`.`BillingAddress_AddressLine2`, `t`.`BillingAddress_ZipCode`, `t`.`BillingAddress_Country_Code`, `t`.`BillingAddress_Country_FullName`, `t`.`ShippingAddress_AddressLine1`, `t`.`ShippingAddress_AddressLine2`, `t`.`ShippingAddress_ZipCode`, `t`.`ShippingAddress_Country_Code`, `t`.`ShippingAddress_Country_FullName` +FROM ( + SELECT `v`.`Id`, `v`.`Name`, `v`.`BillingAddress_AddressLine1`, `v`.`BillingAddress_AddressLine2`, `v`.`BillingAddress_ZipCode`, `v`.`BillingAddress_Country_Code`, `v`.`BillingAddress_Country_FullName`, `v`.`ShippingAddress_AddressLine1`, `v`.`ShippingAddress_AddressLine2`, `v`.`ShippingAddress_ZipCode`, `v`.`ShippingAddress_Country_Code`, `v`.`ShippingAddress_Country_FullName` + FROM `ValuedCustomer` AS `v` + ORDER BY `v`.`Id` + LIMIT 18446744073709551610 OFFSET @__p_0 +) AS `t` +WHERE `t`.`ShippingAddress_ZipCode` = 7728 +"""); + } + + public override async Task Filter_on_property_inside_nested_struct_complex_type_after_subquery(bool async) + { + await base.Filter_on_property_inside_nested_struct_complex_type_after_subquery(async); + + AssertSql( +""" +@__p_0='1' +SELECT DISTINCT `t`.`Id`, `t`.`Name`, `t`.`BillingAddress_AddressLine1`, `t`.`BillingAddress_AddressLine2`, `t`.`BillingAddress_ZipCode`, `t`.`BillingAddress_Country_Code`, `t`.`BillingAddress_Country_FullName`, `t`.`ShippingAddress_AddressLine1`, `t`.`ShippingAddress_AddressLine2`, `t`.`ShippingAddress_ZipCode`, `t`.`ShippingAddress_Country_Code`, `t`.`ShippingAddress_Country_FullName` +FROM ( + SELECT `v`.`Id`, `v`.`Name`, `v`.`BillingAddress_AddressLine1`, `v`.`BillingAddress_AddressLine2`, `v`.`BillingAddress_ZipCode`, `v`.`BillingAddress_Country_Code`, `v`.`BillingAddress_Country_FullName`, `v`.`ShippingAddress_AddressLine1`, `v`.`ShippingAddress_AddressLine2`, `v`.`ShippingAddress_ZipCode`, `v`.`ShippingAddress_Country_Code`, `v`.`ShippingAddress_Country_FullName` + FROM `ValuedCustomer` AS `v` + ORDER BY `v`.`Id` + LIMIT 18446744073709551610 OFFSET @__p_0 +) AS `t` +WHERE `t`.`ShippingAddress_Country_Code` = 'DE' +"""); + } + + public override async Task Filter_on_required_property_inside_required_struct_complex_type_on_optional_navigation(bool async) + { + await base.Filter_on_required_property_inside_required_struct_complex_type_on_optional_navigation(async); + + AssertSql( +""" +SELECT `v`.`Id`, `v`.`OptionalCustomerId`, `v`.`RequiredCustomerId`, `v0`.`Id`, `v0`.`Name`, `v0`.`BillingAddress_AddressLine1`, `v0`.`BillingAddress_AddressLine2`, `v0`.`BillingAddress_ZipCode`, `v0`.`BillingAddress_Country_Code`, `v0`.`BillingAddress_Country_FullName`, `v0`.`ShippingAddress_AddressLine1`, `v0`.`ShippingAddress_AddressLine2`, `v0`.`ShippingAddress_ZipCode`, `v0`.`ShippingAddress_Country_Code`, `v0`.`ShippingAddress_Country_FullName`, `v1`.`Id`, `v1`.`Name`, `v1`.`BillingAddress_AddressLine1`, `v1`.`BillingAddress_AddressLine2`, `v1`.`BillingAddress_ZipCode`, `v1`.`BillingAddress_Country_Code`, `v1`.`BillingAddress_Country_FullName`, `v1`.`ShippingAddress_AddressLine1`, `v1`.`ShippingAddress_AddressLine2`, `v1`.`ShippingAddress_ZipCode`, `v1`.`ShippingAddress_Country_Code`, `v1`.`ShippingAddress_Country_FullName` +FROM `ValuedCustomerGroup` AS `v` +LEFT JOIN `ValuedCustomer` AS `v0` ON `v`.`OptionalCustomerId` = `v0`.`Id` +INNER JOIN `ValuedCustomer` AS `v1` ON `v`.`RequiredCustomerId` = `v1`.`Id` +WHERE (`v0`.`ShippingAddress_ZipCode` <> 7728) OR `v0`.`ShippingAddress_ZipCode` IS NULL +"""); + } + + public override async Task Filter_on_required_property_inside_required_struct_complex_type_on_required_navigation(bool async) + { + await base.Filter_on_required_property_inside_required_struct_complex_type_on_required_navigation(async); + + AssertSql( +""" +SELECT `v`.`Id`, `v`.`OptionalCustomerId`, `v`.`RequiredCustomerId`, `v1`.`Id`, `v1`.`Name`, `v1`.`BillingAddress_AddressLine1`, `v1`.`BillingAddress_AddressLine2`, `v1`.`BillingAddress_ZipCode`, `v1`.`BillingAddress_Country_Code`, `v1`.`BillingAddress_Country_FullName`, `v1`.`ShippingAddress_AddressLine1`, `v1`.`ShippingAddress_AddressLine2`, `v1`.`ShippingAddress_ZipCode`, `v1`.`ShippingAddress_Country_Code`, `v1`.`ShippingAddress_Country_FullName`, `v0`.`Id`, `v0`.`Name`, `v0`.`BillingAddress_AddressLine1`, `v0`.`BillingAddress_AddressLine2`, `v0`.`BillingAddress_ZipCode`, `v0`.`BillingAddress_Country_Code`, `v0`.`BillingAddress_Country_FullName`, `v0`.`ShippingAddress_AddressLine1`, `v0`.`ShippingAddress_AddressLine2`, `v0`.`ShippingAddress_ZipCode`, `v0`.`ShippingAddress_Country_Code`, `v0`.`ShippingAddress_Country_FullName` +FROM `ValuedCustomerGroup` AS `v` +INNER JOIN `ValuedCustomer` AS `v0` ON `v`.`RequiredCustomerId` = `v0`.`Id` +LEFT JOIN `ValuedCustomer` AS `v1` ON `v`.`OptionalCustomerId` = `v1`.`Id` +WHERE `v0`.`ShippingAddress_ZipCode` <> 7728 +"""); + } + + // This test fails because when OptionalCustomer is null, we get all-null results because of the LEFT JOIN, and we materialize this + // as an empty ShippingAddress instead of null (see SQL). The proper solution here would be to project the Customer ID just for the + // purpose of knowing that it's there. + public override async Task Project_struct_complex_type_via_optional_navigation(bool async) + { + var exception = await Assert.ThrowsAsync(() => base.Project_struct_complex_type_via_optional_navigation(async)); + + Assert.Equal(RelationalStrings.CannotProjectNullableComplexType("ValuedCustomer.ShippingAddress#AddressStruct"), exception.Message); + } + + public override async Task Project_struct_complex_type_via_required_navigation(bool async) + { + await base.Project_struct_complex_type_via_required_navigation(async); +AssertSql( +""" +SELECT `v0`.`ShippingAddress_AddressLine1`, `v0`.`ShippingAddress_AddressLine2`, `v0`.`ShippingAddress_ZipCode`, `v0`.`ShippingAddress_Country_Code`, `v0`.`ShippingAddress_Country_FullName` +FROM `ValuedCustomerGroup` AS `v` +INNER JOIN `ValuedCustomer` AS `v0` ON `v`.`RequiredCustomerId` = `v0`.`Id` +"""); + } + + public override async Task Load_struct_complex_type_after_subquery_on_entity_type(bool async) + { + await base.Load_struct_complex_type_after_subquery_on_entity_type(async); + + AssertSql( +""" +@__p_0='1' +SELECT DISTINCT `t`.`Id`, `t`.`Name`, `t`.`BillingAddress_AddressLine1`, `t`.`BillingAddress_AddressLine2`, `t`.`BillingAddress_ZipCode`, `t`.`BillingAddress_Country_Code`, `t`.`BillingAddress_Country_FullName`, `t`.`ShippingAddress_AddressLine1`, `t`.`ShippingAddress_AddressLine2`, `t`.`ShippingAddress_ZipCode`, `t`.`ShippingAddress_Country_Code`, `t`.`ShippingAddress_Country_FullName` +FROM ( + SELECT `v`.`Id`, `v`.`Name`, `v`.`BillingAddress_AddressLine1`, `v`.`BillingAddress_AddressLine2`, `v`.`BillingAddress_ZipCode`, `v`.`BillingAddress_Country_Code`, `v`.`BillingAddress_Country_FullName`, `v`.`ShippingAddress_AddressLine1`, `v`.`ShippingAddress_AddressLine2`, `v`.`ShippingAddress_ZipCode`, `v`.`ShippingAddress_Country_Code`, `v`.`ShippingAddress_Country_FullName` + FROM `ValuedCustomer` AS `v` + ORDER BY `v`.`Id` + LIMIT 18446744073709551610 OFFSET @__p_0 +) AS `t` +"""); + } + + public override async Task Select_struct_complex_type(bool async) + { + await base.Select_struct_complex_type(async); +AssertSql( +""" +SELECT `v`.`ShippingAddress_AddressLine1`, `v`.`ShippingAddress_AddressLine2`, `v`.`ShippingAddress_ZipCode`, `v`.`ShippingAddress_Country_Code`, `v`.`ShippingAddress_Country_FullName` +FROM `ValuedCustomer` AS `v` +"""); + } + + public override async Task Select_nested_struct_complex_type(bool async) + { + await base.Select_nested_struct_complex_type(async); + + AssertSql( +""" +SELECT `v`.`ShippingAddress_Country_Code`, `v`.`ShippingAddress_Country_FullName` +FROM `ValuedCustomer` AS `v` +"""); + } + + public override async Task Select_single_property_on_nested_struct_complex_type(bool async) + { + await base.Select_single_property_on_nested_struct_complex_type(async); + + AssertSql( +""" +SELECT `v`.`ShippingAddress_Country_FullName` +FROM `ValuedCustomer` AS `v` +"""); + } + + public override async Task Select_struct_complex_type_Where(bool async) + { + await base.Select_struct_complex_type_Where(async); +AssertSql( +""" +SELECT `v`.`ShippingAddress_AddressLine1`, `v`.`ShippingAddress_AddressLine2`, `v`.`ShippingAddress_ZipCode`, `v`.`ShippingAddress_Country_Code`, `v`.`ShippingAddress_Country_FullName` +FROM `ValuedCustomer` AS `v` +WHERE `v`.`ShippingAddress_ZipCode` = 7728 +"""); + } + + public override async Task Select_struct_complex_type_Distinct(bool async) + { + await base.Select_struct_complex_type_Distinct(async); +AssertSql( +""" +SELECT DISTINCT `v`.`ShippingAddress_AddressLine1`, `v`.`ShippingAddress_AddressLine2`, `v`.`ShippingAddress_ZipCode`, `v`.`ShippingAddress_Country_Code`, `v`.`ShippingAddress_Country_FullName` +FROM `ValuedCustomer` AS `v` +"""); + } + + public override async Task Struct_complex_type_equals_struct_complex_type(bool async) + { + await base.Struct_complex_type_equals_struct_complex_type(async); + + AssertSql( +""" +SELECT `v`.`Id`, `v`.`Name`, `v`.`BillingAddress_AddressLine1`, `v`.`BillingAddress_AddressLine2`, `v`.`BillingAddress_ZipCode`, `v`.`BillingAddress_Country_Code`, `v`.`BillingAddress_Country_FullName`, `v`.`ShippingAddress_AddressLine1`, `v`.`ShippingAddress_AddressLine2`, `v`.`ShippingAddress_ZipCode`, `v`.`ShippingAddress_Country_Code`, `v`.`ShippingAddress_Country_FullName` +FROM `ValuedCustomer` AS `v` +WHERE ((`v`.`ShippingAddress_AddressLine1` = `v`.`BillingAddress_AddressLine1`) AND ((`v`.`ShippingAddress_AddressLine2` = `v`.`BillingAddress_AddressLine2`) OR (`v`.`ShippingAddress_AddressLine2` IS NULL AND (`v`.`BillingAddress_AddressLine2` IS NULL)))) AND (`v`.`ShippingAddress_ZipCode` = `v`.`BillingAddress_ZipCode`) +"""); + } + + public override async Task Struct_complex_type_equals_constant(bool async) + { + await base.Struct_complex_type_equals_constant(async); +AssertSql( +""" +SELECT `v`.`Id`, `v`.`Name`, `v`.`BillingAddress_AddressLine1`, `v`.`BillingAddress_AddressLine2`, `v`.`BillingAddress_ZipCode`, `v`.`BillingAddress_Country_Code`, `v`.`BillingAddress_Country_FullName`, `v`.`ShippingAddress_AddressLine1`, `v`.`ShippingAddress_AddressLine2`, `v`.`ShippingAddress_ZipCode`, `v`.`ShippingAddress_Country_Code`, `v`.`ShippingAddress_Country_FullName` +FROM `ValuedCustomer` AS `v` +WHERE ((((`v`.`ShippingAddress_AddressLine1` = '804 S. Lakeshore Road') AND `v`.`ShippingAddress_AddressLine2` IS NULL) AND (`v`.`ShippingAddress_ZipCode` = 38654)) AND (`v`.`ShippingAddress_Country_Code` = 'US')) AND (`v`.`ShippingAddress_Country_FullName` = 'United States') +"""); + } + + public override async Task Struct_complex_type_equals_parameter(bool async) + { + await base.Struct_complex_type_equals_parameter(async); + + AssertSql( +""" +@__entity_equality_address_0_AddressLine1='804 S. Lakeshore Road' (Size = 4000) +@__entity_equality_address_0_ZipCode='38654' (Nullable = true) +@__entity_equality_address_0_Code='US' (Size = 4000) +@__entity_equality_address_0_FullName='United States' (Size = 4000) +SELECT `v`.`Id`, `v`.`Name`, `v`.`BillingAddress_AddressLine1`, `v`.`BillingAddress_AddressLine2`, `v`.`BillingAddress_ZipCode`, `v`.`BillingAddress_Country_Code`, `v`.`BillingAddress_Country_FullName`, `v`.`ShippingAddress_AddressLine1`, `v`.`ShippingAddress_AddressLine2`, `v`.`ShippingAddress_ZipCode`, `v`.`ShippingAddress_Country_Code`, `v`.`ShippingAddress_Country_FullName` +FROM `ValuedCustomer` AS `v` +WHERE ((((`v`.`ShippingAddress_AddressLine1` = @__entity_equality_address_0_AddressLine1) AND `v`.`ShippingAddress_AddressLine2` IS NULL) AND (`v`.`ShippingAddress_ZipCode` = @__entity_equality_address_0_ZipCode)) AND (`v`.`ShippingAddress_Country_Code` = @__entity_equality_address_0_Code)) AND (`v`.`ShippingAddress_Country_FullName` = @__entity_equality_address_0_FullName) +"""); + } + + public override async Task Contains_over_struct_complex_type(bool async) + { + await base.Contains_over_struct_complex_type(async); + + AssertSql( +""" +@__entity_equality_address_0_AddressLine1='804 S. Lakeshore Road' (Size = 4000) +@__entity_equality_address_0_ZipCode='38654' (Nullable = true) +@__entity_equality_address_0_Code='US' (Size = 4000) +@__entity_equality_address_0_FullName='United States' (Size = 4000) +SELECT `v`.`Id`, `v`.`Name`, `v`.`BillingAddress_AddressLine1`, `v`.`BillingAddress_AddressLine2`, `v`.`BillingAddress_ZipCode`, `v`.`BillingAddress_Country_Code`, `v`.`BillingAddress_Country_FullName`, `v`.`ShippingAddress_AddressLine1`, `v`.`ShippingAddress_AddressLine2`, `v`.`ShippingAddress_ZipCode`, `v`.`ShippingAddress_Country_Code`, `v`.`ShippingAddress_Country_FullName` +FROM `ValuedCustomer` AS `v` +WHERE EXISTS ( + SELECT 1 + FROM `ValuedCustomer` AS `v0` + WHERE ((((`v0`.`ShippingAddress_AddressLine1` = @__entity_equality_address_0_AddressLine1) AND `v0`.`ShippingAddress_AddressLine2` IS NULL) AND (`v0`.`ShippingAddress_ZipCode` = @__entity_equality_address_0_ZipCode)) AND (`v0`.`ShippingAddress_Country_Code` = @__entity_equality_address_0_Code)) AND (`v0`.`ShippingAddress_Country_FullName` = @__entity_equality_address_0_FullName)) +"""); + } + + public override async Task Concat_entity_type_containing_struct_complex_property(bool async) + { + await base.Concat_entity_type_containing_struct_complex_property(async); + + AssertSql( +""" +SELECT `v`.`Id`, `v`.`Name`, `v`.`BillingAddress_AddressLine1`, `v`.`BillingAddress_AddressLine2`, `v`.`BillingAddress_ZipCode`, `v`.`BillingAddress_Country_Code`, `v`.`BillingAddress_Country_FullName`, `v`.`ShippingAddress_AddressLine1`, `v`.`ShippingAddress_AddressLine2`, `v`.`ShippingAddress_ZipCode`, `v`.`ShippingAddress_Country_Code`, `v`.`ShippingAddress_Country_FullName` +FROM `ValuedCustomer` AS `v` +WHERE `v`.`Id` = 1 +UNION ALL +SELECT `v0`.`Id`, `v0`.`Name`, `v0`.`BillingAddress_AddressLine1`, `v0`.`BillingAddress_AddressLine2`, `v0`.`BillingAddress_ZipCode`, `v0`.`BillingAddress_Country_Code`, `v0`.`BillingAddress_Country_FullName`, `v0`.`ShippingAddress_AddressLine1`, `v0`.`ShippingAddress_AddressLine2`, `v0`.`ShippingAddress_ZipCode`, `v0`.`ShippingAddress_Country_Code`, `v0`.`ShippingAddress_Country_FullName` +FROM `ValuedCustomer` AS `v0` +WHERE `v0`.`Id` = 2 +"""); + } + + public override async Task Union_entity_type_containing_struct_complex_property(bool async) + { + await base.Union_entity_type_containing_struct_complex_property(async); + + AssertSql( +""" +SELECT `v`.`Id`, `v`.`Name`, `v`.`BillingAddress_AddressLine1`, `v`.`BillingAddress_AddressLine2`, `v`.`BillingAddress_ZipCode`, `v`.`BillingAddress_Country_Code`, `v`.`BillingAddress_Country_FullName`, `v`.`ShippingAddress_AddressLine1`, `v`.`ShippingAddress_AddressLine2`, `v`.`ShippingAddress_ZipCode`, `v`.`ShippingAddress_Country_Code`, `v`.`ShippingAddress_Country_FullName` +FROM `ValuedCustomer` AS `v` +WHERE `v`.`Id` = 1 +UNION +SELECT `v0`.`Id`, `v0`.`Name`, `v0`.`BillingAddress_AddressLine1`, `v0`.`BillingAddress_AddressLine2`, `v0`.`BillingAddress_ZipCode`, `v0`.`BillingAddress_Country_Code`, `v0`.`BillingAddress_Country_FullName`, `v0`.`ShippingAddress_AddressLine1`, `v0`.`ShippingAddress_AddressLine2`, `v0`.`ShippingAddress_ZipCode`, `v0`.`ShippingAddress_Country_Code`, `v0`.`ShippingAddress_Country_FullName` +FROM `ValuedCustomer` AS `v0` +WHERE `v0`.`Id` = 2 +"""); + } + + public override async Task Concat_struct_complex_type(bool async) + { + await base.Concat_struct_complex_type(async); + + AssertSql( +""" +SELECT `v`.`ShippingAddress_AddressLine1`, `v`.`ShippingAddress_AddressLine2`, `v`.`ShippingAddress_ZipCode`, `v`.`ShippingAddress_Country_Code`, `v`.`ShippingAddress_Country_FullName` +FROM `ValuedCustomer` AS `v` +WHERE `v`.`Id` = 1 +UNION ALL +SELECT `v0`.`ShippingAddress_AddressLine1`, `v0`.`ShippingAddress_AddressLine2`, `v0`.`ShippingAddress_ZipCode`, `v0`.`ShippingAddress_Country_Code`, `v0`.`ShippingAddress_Country_FullName` +FROM `ValuedCustomer` AS `v0` +WHERE `v0`.`Id` = 2 +"""); + } + + public override async Task Union_struct_complex_type(bool async) + { + await base.Union_struct_complex_type(async); + + AssertSql( +""" +SELECT `v`.`ShippingAddress_AddressLine1`, `v`.`ShippingAddress_AddressLine2`, `v`.`ShippingAddress_ZipCode`, `v`.`ShippingAddress_Country_Code`, `v`.`ShippingAddress_Country_FullName` +FROM `ValuedCustomer` AS `v` +WHERE `v`.`Id` = 1 +UNION +SELECT `v0`.`ShippingAddress_AddressLine1`, `v0`.`ShippingAddress_AddressLine2`, `v0`.`ShippingAddress_ZipCode`, `v0`.`ShippingAddress_Country_Code`, `v0`.`ShippingAddress_Country_FullName` +FROM `ValuedCustomer` AS `v0` +WHERE `v0`.`Id` = 2 +"""); + } + + public override async Task Concat_property_in_struct_complex_type(bool async) + { + await base.Concat_property_in_struct_complex_type(async); +AssertSql( +""" +SELECT `v`.`ShippingAddress_AddressLine1` +FROM `ValuedCustomer` AS `v` +UNION ALL +SELECT `v0`.`BillingAddress_AddressLine1` AS `ShippingAddress_AddressLine1` +FROM `ValuedCustomer` AS `v0` +"""); + } + + public override async Task Union_property_in_struct_complex_type(bool async) + { + await base.Union_property_in_struct_complex_type(async); + + AssertSql( +""" +SELECT `v`.`ShippingAddress_AddressLine1` +FROM `ValuedCustomer` AS `v` +UNION +SELECT `v0`.`BillingAddress_AddressLine1` AS `ShippingAddress_AddressLine1` +FROM `ValuedCustomer` AS `v0` +"""); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + public class ComplexTypeQuerySingleStoreFixture : ComplexTypeQueryRelationalFixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => SingleStoreTestStoreFactory.Instance; + } +} diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/Ef6GroupBySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/Ef6GroupBySingleStoreTest.cs index aa1e3581b..21dab32fb 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/Ef6GroupBySingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/Ef6GroupBySingleStoreTest.cs @@ -752,7 +752,7 @@ LEFT JOIN ( """); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override async Task Whats_new_2021_sample_7(bool async) { await base.Whats_new_2021_sample_7(async); @@ -800,7 +800,7 @@ GROUP BY `p`.`Category` """); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override async Task Whats_new_2021_sample_9(bool async) { await base.Whats_new_2021_sample_9(async); @@ -829,7 +829,7 @@ GROUP BY `p`.`Category` """); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override async Task Whats_new_2021_sample_4(bool async) { await base.Whats_new_2021_sample_4(async); diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/EscapesSingleStoreNoBackslashesTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/EscapesSingleStoreNoBackslashesTest.cs index fbbf3aa72..40ea59c14 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/EscapesSingleStoreNoBackslashesTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/EscapesSingleStoreNoBackslashesTest.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.TestUtilities; using EntityFrameworkCore.SingleStore.FunctionalTests.TestUtilities; +using EntityFrameworkCore.SingleStore.Infrastructure.Internal; using EntityFrameworkCore.SingleStore.Tests; using Xunit; using Xunit.Abstractions; @@ -80,10 +81,30 @@ public override async Task Where_contains_query_escapes(bool async) { await base.Where_contains_query_escapes(async); - AssertSql( - @"SELECT `a`.`ArtistId`, `a`.`Name` -FROM `Artists` AS `a` -WHERE `a`.`Name` IN ('Back\slasher''s', 'John''s Chill Box')"); + if (SingleStoreTestHelpers.HasPrimitiveCollectionsSupport(Fixture)) + { + AssertSql( + """ + SELECT `a`.`ArtistId`, `a`.`Name` + FROM `Artists` AS `a` + WHERE `a`.`Name` IN ( + SELECT `a0`.`value` + FROM JSON_TABLE('["Back\\slasher\u0027s","John\u0027s Chill Box"]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` longtext PATH '$[0]' + )) AS `a0` + ) + """); + } + else + { + AssertSql( + """ + SELECT `a`.`ArtistId`, `a`.`Name` + FROM `Artists` AS `a` + WHERE `a`.`Name` IN ('Back\slasher''s', 'John''s Chill Box') + """); + } } public class EscapesSingleStoreNoBackslashesFixture : EscapesSingleStoreFixtureBase diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/EscapesSingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/EscapesSingleStoreTest.cs index 40d8b09a6..a70ca3f03 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/EscapesSingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/EscapesSingleStoreTest.cs @@ -79,10 +79,30 @@ public override async Task Where_contains_query_escapes(bool async) { await base.Where_contains_query_escapes(async); - AssertSql( - @"SELECT `a`.`ArtistId`, `a`.`Name` -FROM `Artists` AS `a` -WHERE `a`.`Name` IN ('Back\\slasher''s', 'John''s Chill Box')"); + if (SingleStoreTestHelpers.HasPrimitiveCollectionsSupport(Fixture)) + { + AssertSql( + """ + SELECT `a`.`ArtistId`, `a`.`Name` + FROM `Artists` AS `a` + WHERE `a`.`Name` IN ( + SELECT `a0`.`value` + FROM JSON_TABLE('["Back\\\\slasher\\u0027s","John\\u0027s Chill Box"]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` longtext PATH '$[0]' + )) AS `a0` + ) + """); + } + else + { + AssertSql( + """ + SELECT `a`.`ArtistId`, `a`.`Name` + FROM `Artists` AS `a` + WHERE `a`.`Name` IN ('Back\\slasher''s', 'John''s Chill Box') + """); + } } public class EscapesSingleStoreFixture : EscapesSingleStoreFixtureBase diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/FiltersInheritanceQuerySingleStoreFixture.cs b/test/EFCore.SingleStore.FunctionalTests/Query/FiltersInheritanceQuerySingleStoreFixture.cs deleted file mode 100644 index 1fde92ca5..000000000 --- a/test/EFCore.SingleStore.FunctionalTests/Query/FiltersInheritanceQuerySingleStoreFixture.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.TestModels.InheritanceModel; - -namespace EntityFrameworkCore.SingleStore.FunctionalTests.Query -{ - public class FiltersInheritanceQuerySingleStoreFixture : InheritanceQuerySingleStoreFixture - { - protected override bool EnableFilters => true; - - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - base.OnModelCreating(modelBuilder, context); - - // We're changing the data type of the fields from INT to BIGINT, because in SingleStore - // on a sharded (distributed) table, AUTO_INCREMENT can only be used on a BIGINT column - modelBuilder.Entity() - .Property(e => e.Id) - .HasColumnType("bigint"); - - modelBuilder.Entity() - .Property(e => e.Id) - .HasColumnType("bigint"); - - modelBuilder.Entity() - .Property(e => e.Id) - .HasColumnType("bigint"); - } - } -} diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/FiltersInheritanceQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/FiltersInheritanceQuerySingleStoreTest.cs deleted file mode 100644 index c03192e32..000000000 --- a/test/EFCore.SingleStore.FunctionalTests/Query/FiltersInheritanceQuerySingleStoreTest.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Linq; -using System.Threading.Tasks; -using EntityFrameworkCore.SingleStore.Tests; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Query; -using Microsoft.EntityFrameworkCore.TestModels.InheritanceModel; -using Xunit; -using Xunit.Abstractions; - -namespace EntityFrameworkCore.SingleStore.FunctionalTests.Query -{ - public class FiltersInheritanceQuerySingleStoreTest : FiltersInheritanceQueryTestBase - { - public FiltersInheritanceQuerySingleStoreTest(FiltersInheritanceQuerySingleStoreFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - - [ConditionalTheory] - public override async Task Can_use_IgnoreQueryFilters_and_GetDatabaseValues(bool async) - { - // We're skipping this test when we're running tests on Managed Service due to the specifics of - // how AUTO_INCREMENT works (https://docs.singlestore.com/cloud/reference/sql-reference/data-definition-language-ddl/create-table/#auto-increment-behavior) - if (AppConfig.ManagedService) - { - return; - } - - await base.Can_use_IgnoreQueryFilters_and_GetDatabaseValues(async); - } - } -} diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/FunkyDataQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/FunkyDataQuerySingleStoreTest.cs index 2724c29a8..391f09868 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/FunkyDataQuerySingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/FunkyDataQuerySingleStoreTest.cs @@ -1,33 +1,192 @@ -using EntityFrameworkCore.SingleStore.FunctionalTests.TestUtilities; +using System.Threading.Tasks; +using EntityFrameworkCore.SingleStore.FunctionalTests.TestUtilities; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.TestUtilities; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace EntityFrameworkCore.SingleStore.FunctionalTests.Query +namespace EntityFrameworkCore.SingleStore.FunctionalTests.Query; + +public class FunkyDataQuerySingleStoreTest : FunkyDataQueryTestBase { - public class FunkyDataQuerySingleStoreTest : FunkyDataQueryTestBase + public FunkyDataQuerySingleStoreTest(FunkyDataQuerySingleStoreFixture fixture) + : base(fixture) + { + ClearLog(); + //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task String_contains_on_argument_with_wildcard_constant(bool async) + { + await base.String_contains_on_argument_with_wildcard_constant(async); + + AssertSql( +""" +SELECT `f`.`FirstName` +FROM `FunkyCustomers` AS `f` +WHERE `f`.`FirstName` LIKE '%\\%B%' +""", + // + """ +SELECT `f`.`FirstName` +FROM `FunkyCustomers` AS `f` +WHERE `f`.`FirstName` LIKE '%a\\_%' +""", + // + """ +SELECT `f`.`FirstName` +FROM `FunkyCustomers` AS `f` +WHERE FALSE +""", + // + """ +SELECT `f`.`FirstName` +FROM `FunkyCustomers` AS `f` +WHERE `f`.`FirstName` IS NOT NULL +""", + // + """ +SELECT `f`.`FirstName` +FROM `FunkyCustomers` AS `f` +WHERE `f`.`FirstName` LIKE '%\\_Ba\\_%' +""", + // + """ +SELECT `f`.`FirstName` +FROM `FunkyCustomers` AS `f` +WHERE `f`.`FirstName` NOT LIKE '%\\%B\\%a\\%r%' OR (`f`.`FirstName` IS NULL) +""", + // + """ +SELECT `f`.`FirstName` +FROM `FunkyCustomers` AS `f` +WHERE `f`.`FirstName` IS NULL +""", + // + """ +SELECT `f`.`FirstName` +FROM `FunkyCustomers` AS `f` +"""); + } + + public override async Task String_starts_with_on_argument_with_wildcard_constant(bool async) { - public FunkyDataQuerySingleStoreTest(FunkyDataQuerySingleStoreFixture fixture) - : base(fixture) - { - ClearLog(); - //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - protected override void ClearLog() - => Fixture.TestSqlLoggerFactory.Clear(); - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - - public class FunkyDataQuerySingleStoreFixture : FunkyDataQueryFixtureBase - { - public TestSqlLoggerFactory TestSqlLoggerFactory - => (TestSqlLoggerFactory)ServiceProvider.GetRequiredService(); - - protected override ITestStoreFactory TestStoreFactory - => SingleStoreTestStoreFactory.Instance; - } + await base.String_starts_with_on_argument_with_wildcard_constant(async); + + AssertSql( +""" +SELECT `f`.`FirstName` +FROM `FunkyCustomers` AS `f` +WHERE `f`.`FirstName` LIKE '\\%B%' +""", + // +""" +SELECT `f`.`FirstName` +FROM `FunkyCustomers` AS `f` +WHERE `f`.`FirstName` LIKE '\\_B%' +""", + // +""" +SELECT `f`.`FirstName` +FROM `FunkyCustomers` AS `f` +WHERE FALSE +""", + // +""" +SELECT `f`.`FirstName` +FROM `FunkyCustomers` AS `f` +WHERE `f`.`FirstName` IS NOT NULL +""", + // +""" +SELECT `f`.`FirstName` +FROM `FunkyCustomers` AS `f` +WHERE `f`.`FirstName` LIKE '\\_Ba\\_%' +""", + // +""" +SELECT `f`.`FirstName` +FROM `FunkyCustomers` AS `f` +WHERE `f`.`FirstName` NOT LIKE '\\%B\\%a\\%r%' OR (`f`.`FirstName` IS NULL) +""", + // +""" +SELECT `f`.`FirstName` +FROM `FunkyCustomers` AS `f` +WHERE `f`.`FirstName` IS NULL +""", + // +""" +SELECT `f`.`FirstName` +FROM `FunkyCustomers` AS `f` +"""); + } + + public override async Task String_ends_with_on_argument_with_wildcard_constant(bool async) + { + await base.String_ends_with_on_argument_with_wildcard_constant(async); + + AssertSql( +""" +SELECT `f`.`FirstName` +FROM `FunkyCustomers` AS `f` +WHERE `f`.`FirstName` LIKE '%\\%r' +""", + // +""" +SELECT `f`.`FirstName` +FROM `FunkyCustomers` AS `f` +WHERE `f`.`FirstName` LIKE '%r\\_' +""", + // +""" +SELECT `f`.`FirstName` +FROM `FunkyCustomers` AS `f` +WHERE FALSE +""", + // +""" +SELECT `f`.`FirstName` +FROM `FunkyCustomers` AS `f` +WHERE `f`.`FirstName` IS NOT NULL +""", + // +""" +SELECT `f`.`FirstName` +FROM `FunkyCustomers` AS `f` +WHERE `f`.`FirstName` LIKE '%\\_r\\_' +""", + // +""" +SELECT `f`.`FirstName` +FROM `FunkyCustomers` AS `f` +WHERE `f`.`FirstName` NOT LIKE '%a\\%r\\%' OR (`f`.`FirstName` IS NULL) +""", + // +""" +SELECT `f`.`FirstName` +FROM `FunkyCustomers` AS `f` +WHERE `f`.`FirstName` IS NULL +""", + // +""" +SELECT `f`.`FirstName` +FROM `FunkyCustomers` AS `f` +"""); + } + + protected override void ClearLog() + => Fixture.TestSqlLoggerFactory.Clear(); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + public class FunkyDataQuerySingleStoreFixture : FunkyDataQueryFixtureBase + { + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ServiceProvider.GetRequiredService(); + + protected override ITestStoreFactory TestStoreFactory + => SingleStoreTestStoreFactory.Instance; } } diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/GearsOfWarQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/GearsOfWarQuerySingleStoreTest.cs index 5e915a58a..35df4d23d 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/GearsOfWarQuerySingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/GearsOfWarQuerySingleStoreTest.cs @@ -231,7 +231,7 @@ public override Task Subquery_projecting_non_nullable_scalar_contains_non_nullab return base.Subquery_projecting_non_nullable_scalar_contains_non_nullable_value_doesnt_need_null_expansion(async); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override Task Conditional_expression_with_test_being_simplified_to_constant_complex(bool isAsync) { return base.Conditional_expression_with_test_being_simplified_to_constant_complex(isAsync); @@ -249,19 +249,19 @@ public override Task Correlated_collection_with_very_complex_order_by(bool async return base.Correlated_collection_with_very_complex_order_by(async); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override Task Correlated_collections_with_FirstOrDefault(bool async) { return base.Correlated_collections_with_FirstOrDefault(async); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override Task FirstOrDefault_on_empty_collection_of_DateTime_in_subquery(bool async) { return base.FirstOrDefault_on_empty_collection_of_DateTime_in_subquery(async); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override Task FirstOrDefault_over_int_compared_to_zero(bool async) { return base.FirstOrDefault_over_int_compared_to_zero(async); @@ -291,61 +291,61 @@ public override Task Include_with_complex_order_by(bool async) return base.Include_with_complex_order_by(async); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override Task Multiple_orderby_with_navigation_expansion_on_one_of_the_order_bys_inside_subquery_complex_orderings(bool async) { return base.Multiple_orderby_with_navigation_expansion_on_one_of_the_order_bys_inside_subquery_complex_orderings(async); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override Task Optional_navigation_with_collection_composite_key(bool async) { return base.Optional_navigation_with_collection_composite_key(async); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override Task Project_one_value_type_converted_to_nullable_from_empty_collection(bool async) { return base.Project_one_value_type_converted_to_nullable_from_empty_collection(async); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override Task Project_one_value_type_from_empty_collection(bool async) { return base.Project_one_value_type_from_empty_collection(async); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override Task Query_with_complex_let_containing_ordering_and_filter_projecting_firstOrDefault_element_of_let(bool async) { return base.Query_with_complex_let_containing_ordering_and_filter_projecting_firstOrDefault_element_of_let(async); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override Task Select_subquery_boolean(bool async) { return base.Select_subquery_boolean(async); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override Task Select_subquery_boolean_with_pushdown(bool async) { return base.Select_subquery_boolean_with_pushdown(async); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override Task Select_subquery_int_with_inside_cast_and_coalesce(bool async) { return base.Select_subquery_int_with_inside_cast_and_coalesce(async); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override Task Select_subquery_int_with_outside_cast_and_coalesce(bool async) { return base.Select_subquery_int_with_outside_cast_and_coalesce(async); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override Task Select_subquery_int_with_pushdown_and_coalesce(bool async) { return base.Select_subquery_int_with_pushdown_and_coalesce(async); @@ -440,5 +440,128 @@ public override Task Correlated_collection_with_groupby_with_complex_grouping_ke { return base.Correlated_collection_with_groupby_with_complex_grouping_key_not_projecting_identifier_column_with_group_aggregate_in_final_projection(async); } + + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed.")] + public override Task Where_subquery_with_ElementAtOrDefault_equality_to_null_with_composite_key(bool async) + { + return base.Where_subquery_with_ElementAtOrDefault_equality_to_null_with_composite_key(async); + } + + public override async Task Group_by_on_StartsWith_with_null_parameter_as_argument(bool async) + { + await base.Group_by_on_StartsWith_with_null_parameter_as_argument(async); + + AssertSql( +""" +SELECT `t`.`Key` +FROM ( + SELECT FALSE AS `Key` + FROM `Gears` AS `g` +) AS `t` +GROUP BY `t`.`Key` +"""); + } + + public override async Task Array_access_on_byte_array(bool async) + { + await base.Array_access_on_byte_array(async); + + AssertSql( +""" +SELECT `s`.`Id`, `s`.`Banner`, `s`.`Banner5`, `s`.`InternalNumber`, `s`.`Name` +FROM `Squads` AS `s` +WHERE ASCII(SUBSTRING(`s`.`Banner5`, 2 + 1, 1)) = 6 +"""); + } + + public override async Task DateTimeOffset_to_unix_time_milliseconds(bool async) + { + await base.DateTimeOffset_to_unix_time_milliseconds(async); + + AssertSql( +""" +@__unixEpochMilliseconds_0='0' + +SELECT `g`.`Nickname`, `g`.`SquadId`, `g`.`AssignedCityName`, `g`.`CityOfBirthName`, `g`.`Discriminator`, `g`.`FullName`, `g`.`HasSoulPatch`, `g`.`LeaderNickname`, `g`.`LeaderSquadId`, `g`.`Rank`, `s`.`Id`, `s`.`Banner`, `s`.`Banner5`, `s`.`InternalNumber`, `s`.`Name`, `s1`.`SquadId`, `s1`.`MissionId` +FROM `Gears` AS `g` +INNER JOIN `Squads` AS `s` ON `g`.`SquadId` = `s`.`Id` +LEFT JOIN `SquadMissions` AS `s1` ON `s`.`Id` = `s1`.`SquadId` +WHERE NOT EXISTS ( + SELECT 1 + FROM `SquadMissions` AS `s0` + INNER JOIN `Missions` AS `m` ON `s0`.`MissionId` = `m`.`Id` + WHERE (`s`.`Id` = `s0`.`SquadId`) AND (@__unixEpochMilliseconds_0 = (TIMESTAMPDIFF(microsecond, '1970-01-01 00:00:00', `m`.`Timeline`)) DIV (1000))) +ORDER BY `g`.`Nickname`, `g`.`SquadId`, `s`.`Id`, `s1`.`SquadId` +"""); + } + + public override async Task DateTimeOffset_to_unix_time_seconds(bool async) + { + await base.DateTimeOffset_to_unix_time_seconds(async); + + AssertSql( +""" +@__unixEpochSeconds_0='0' + +SELECT `g`.`Nickname`, `g`.`SquadId`, `g`.`AssignedCityName`, `g`.`CityOfBirthName`, `g`.`Discriminator`, `g`.`FullName`, `g`.`HasSoulPatch`, `g`.`LeaderNickname`, `g`.`LeaderSquadId`, `g`.`Rank`, `s`.`Id`, `s`.`Banner`, `s`.`Banner5`, `s`.`InternalNumber`, `s`.`Name`, `s1`.`SquadId`, `s1`.`MissionId` +FROM `Gears` AS `g` +INNER JOIN `Squads` AS `s` ON `g`.`SquadId` = `s`.`Id` +LEFT JOIN `SquadMissions` AS `s1` ON `s`.`Id` = `s1`.`SquadId` +WHERE NOT EXISTS ( + SELECT 1 + FROM `SquadMissions` AS `s0` + INNER JOIN `Missions` AS `m` ON `s0`.`MissionId` = `m`.`Id` + WHERE (`s`.`Id` = `s0`.`SquadId`) AND (@__unixEpochSeconds_0 = TIMESTAMPDIFF(second, '1970-01-01 00:00:00', `m`.`Timeline`))) +ORDER BY `g`.`Nickname`, `g`.`SquadId`, `s`.`Id`, `s1`.`SquadId` +"""); + } + + public override async Task Group_by_with_having_StartsWith_with_null_parameter_as_argument(bool async) + { + await base.Group_by_with_having_StartsWith_with_null_parameter_as_argument(async); + + AssertSql( +""" +SELECT `g`.`FullName` +FROM `Gears` AS `g` +GROUP BY `g`.`FullName` +HAVING FALSE +"""); + } + + public override async Task Select_StartsWith_with_null_parameter_as_argument(bool async) + { + await base.Select_StartsWith_with_null_parameter_as_argument(async); + + AssertSql( +""" +SELECT FALSE +FROM `Gears` AS `g` +"""); + } + + [SupportedServerVersionCondition(nameof(ServerVersionSupport.LimitWithNonConstantValue))] + public override async Task Where_subquery_with_ElementAt_using_column_as_index(bool async) + { + await base.Where_subquery_with_ElementAt_using_column_as_index(async); + + AssertSql(""); + } + + public override async Task Where_datetimeoffset_hour_component(bool async) + { + await AssertQuery( + async, + ss => from m in ss.Set() + where m.Timeline.Hour == /* 10 */ 8 + select m); + + AssertSql( +""" +SELECT `m`.`Id`, `m`.`CodeName`, `m`.`Date`, `m`.`Duration`, `m`.`Rating`, `m`.`Time`, `m`.`Timeline` +FROM `Missions` AS `m` +WHERE EXTRACT(hour FROM `m`.`Timeline`) = 8 +"""); + } } } diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/InheritanceQuerySingleStoreFixture.cs b/test/EFCore.SingleStore.FunctionalTests/Query/InheritanceQuerySingleStoreFixture.cs deleted file mode 100644 index c52b24981..000000000 --- a/test/EFCore.SingleStore.FunctionalTests/Query/InheritanceQuerySingleStoreFixture.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using EntityFrameworkCore.SingleStore.FunctionalTests.TestUtilities; -using Microsoft.EntityFrameworkCore.Query; -using Microsoft.EntityFrameworkCore.TestModels.InheritanceModel; -using Microsoft.EntityFrameworkCore.TestUtilities; - -namespace EntityFrameworkCore.SingleStore.FunctionalTests.Query -{ - public class InheritanceQuerySingleStoreFixture : InheritanceQueryRelationalFixture - { - protected override ITestStoreFactory TestStoreFactory - => SingleStoreTestStoreFactory.Instance; - - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - base.OnModelCreating(modelBuilder, context); - - // We're changing the data type of the fields from INT to BIGINT, because in SingleStore - // on a sharded (distributed) table, AUTO_INCREMENT can only be used on a BIGINT column - modelBuilder.Entity() - .Property(e => e.Id) - .HasColumnType("bigint"); - - modelBuilder.Entity() - .Property(e => e.Id) - .HasColumnType("bigint"); - - modelBuilder.Entity() - .Property(e => e.Id) - .HasColumnType("bigint"); - -#pragma warning disable CS0618 // Type or member is obsolete - modelBuilder.Entity() - .HasNoKey() - .ToQuery( - () => context.Set() - .FromSqlRaw(@"SELECT * FROM `Animals`")); -#pragma warning restore CS0618 // Type or member is obsolete - } - } -} diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/InheritanceQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/InheritanceQuerySingleStoreTest.cs deleted file mode 100644 index fb566fda9..000000000 --- a/test/EFCore.SingleStore.FunctionalTests/Query/InheritanceQuerySingleStoreTest.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Linq; -using System.Threading.Tasks; -using EntityFrameworkCore.SingleStore.Tests; -using Microsoft.EntityFrameworkCore.Query; -using Microsoft.EntityFrameworkCore.TestModels.InheritanceModel; -using Xunit; -using Xunit.Abstractions; - -namespace EntityFrameworkCore.SingleStore.FunctionalTests.Query -{ - public class InheritanceQuerySingleStoreTest : InheritanceRelationalQueryTestBase - { - public InheritanceQuerySingleStoreTest(InheritanceQuerySingleStoreFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - [ConditionalTheory(Skip = "https://github.com/mysql-net/SingleStoreConnector/pull/896")] - public override Task Byte_enum_value_constant_used_in_projection(bool async) - { - return base.Byte_enum_value_constant_used_in_projection(async); - } - - [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore.")] - public override void Setting_foreign_key_to_a_different_type_throws() - { - base.Setting_foreign_key_to_a_different_type_throws(); - } - - [ConditionalFact] - public override void Can_insert_update_delete() - { - // We're skipping this test when we're running tests on Managed Service due to the specifics of - // how AUTO_INCREMENT works (https://docs.singlestore.com/cloud/reference/sql-reference/data-definition-language-ddl/create-table/#auto-increment-behavior) - if (AppConfig.ManagedService) - { - return; - } - - base.Can_insert_update_delete(); - } - - [ConditionalTheory] - public override Task Can_include_prey(bool async) - { - // We're skipping this test when we're running tests on Managed Service due to the specifics of - // how AUTO_INCREMENT works (https://docs.singlestore.com/cloud/reference/sql-reference/data-definition-language-ddl/create-table/#auto-increment-behavior) - if (AppConfig.ManagedService) - { - return Task.CompletedTask; - } - - return base.Can_include_prey(async); - } - } -} diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/MatchQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/MatchQuerySingleStoreTest.cs index 0f90e1a85..cf39042c3 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/MatchQuerySingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/MatchQuerySingleStoreTest.cs @@ -21,169 +21,338 @@ public MatchQuerySingleStoreTest(MatchQuerySingleStoreFixture fixture) public virtual void Match() { using var context = CreateContext(); - var count = context.Set().Count(herb => EF.Functions.Match(herb.Name, "First")); + var count = context.Set().Count(herb => EF.Functions.Match(herb.Name, "First") > 0); Assert.Equal(3, count); AssertSql(@"SELECT COUNT(*) FROM `Herb` AS `h` -WHERE MATCH (`h`.`Name`) AGAINST ('First')"); +WHERE MATCH (`h`.`Name`) AGAINST ('First') > 0.0"); + } + + [ConditionalFact] + public virtual void IsMatch() + { + using var context = CreateContext(); + var count = context.Set().Count(herb => EF.Functions.IsMatch(herb.Name, "First")); + + Assert.Equal(3, count); + + AssertSql(@"SELECT COUNT(*) +FROM `Herb` AS `h` +WHERE MATCH (`h`.`Name`) AGAINST ('First') > 0.0"); } [ConditionalFact] public virtual void Match_multiple_columns() { using var context = CreateContext(); - var count = context.Set().Count(herb => EF.Functions.Match(new []{herb.Name, herb.Garden}, "First")); + var count = context.Set().Count(herb => EF.Functions.Match(new []{herb.Name, herb.Garden}, "First") > 0); Assert.Equal(5, count); AssertSql(@"SELECT COUNT(*) FROM `Herb` AS `h` -WHERE MATCH (`h`.`Name`, `h`.`Garden`) AGAINST ('First')"); +WHERE MATCH (`h`.`Name`, `h`.`Garden`) AGAINST ('First') > 0.0"); + } + + [ConditionalFact] + public virtual void IsMatch_multiple_columns() + { + using var context = CreateContext(); + var count = context.Set().Count(herb => EF.Functions.IsMatch(new []{herb.Name, herb.Garden}, "First")); + + Assert.Equal(5, count); + + AssertSql(@"SELECT COUNT(*) +FROM `Herb` AS `h` +WHERE MATCH (`h`.`Name`, `h`.`Garden`) AGAINST ('First') > 0.0"); } [ConditionalFact] public virtual void Match_keywords_separated() { using var context = CreateContext(); - var count = context.Set().Count(herb => EF.Functions.Match(herb.Name, "First, Second")); + var count = context.Set().Count(herb => EF.Functions.Match(herb.Name, "First, Second") > 0); + + Assert.Equal(6, count); + + AssertSql(@"SELECT COUNT(*) +FROM `Herb` AS `h` +WHERE MATCH (`h`.`Name`) AGAINST ('First, Second') > 0.0"); + } + + [ConditionalFact] + public virtual void IsMatch_keywords_separated() + { + using var context = CreateContext(); + var count = context.Set().Count(herb => EF.Functions.IsMatch(herb.Name, "First, Second")); Assert.Equal(6, count); AssertSql(@"SELECT COUNT(*) FROM `Herb` AS `h` -WHERE MATCH (`h`.`Name`) AGAINST ('First, Second')"); +WHERE MATCH (`h`.`Name`) AGAINST ('First, Second') > 0.0"); } [ConditionalFact] public virtual void Match_keywords_separated_multiple_columns() { using var context = CreateContext(); - var count = context.Set().Count(herb => EF.Functions.Match(new []{herb.Name, herb.Garden}, "First, Second")); + var count = context.Set().Count(herb => EF.Functions.Match(new []{herb.Name, herb.Garden}, "First, Second") > 0); Assert.Equal(8, count); AssertSql(@"SELECT COUNT(*) FROM `Herb` AS `h` -WHERE MATCH (`h`.`Name`, `h`.`Garden`) AGAINST ('First, Second')"); +WHERE MATCH (`h`.`Name`, `h`.`Garden`) AGAINST ('First, Second') > 0.0"); + } + + [ConditionalFact] + public virtual void IsMatch_keywords_separated_multiple_columns() + { + using var context = CreateContext(); + var count = context.Set().Count(herb => EF.Functions.IsMatch(new []{herb.Name, herb.Garden}, "First, Second")); + + Assert.Equal(8, count); + + AssertSql(@"SELECT COUNT(*) +FROM `Herb` AS `h` +WHERE MATCH (`h`.`Name`, `h`.`Garden`) AGAINST ('First, Second') > 0.0"); } [ConditionalFact] public virtual void Match_multiple_keywords() { using var context = CreateContext(); - var count = context.Set().Count(herb => EF.Functions.Match(herb.Name, "First Herb")); + var count = context.Set().Count(herb => EF.Functions.Match(herb.Name, "First Herb") > 0); + + Assert.Equal(9, count); + + AssertSql(@"SELECT COUNT(*) +FROM `Herb` AS `h` +WHERE MATCH (`h`.`Name`) AGAINST ('First Herb') > 0.0"); + } + + [ConditionalFact] + public virtual void IsMatch_multiple_keywords() + { + using var context = CreateContext(); + var count = context.Set().Count(herb => EF.Functions.IsMatch(herb.Name, "First Herb")); Assert.Equal(9, count); AssertSql(@"SELECT COUNT(*) FROM `Herb` AS `h` -WHERE MATCH (`h`.`Name`) AGAINST ('First Herb')"); +WHERE MATCH (`h`.`Name`) AGAINST ('First Herb') > 0.0"); } [ConditionalFact] public virtual void Match_multiple_keywords_multiple_columns() { using var context = CreateContext(); - var count = context.Set().Count(herb => EF.Functions.Match(new []{herb.Name, herb.Garden}, "First Herb")); + var count = context.Set().Count(herb => EF.Functions.Match(new []{herb.Name, herb.Garden}, "First Herb") > 0); + + Assert.Equal(9, count); + + AssertSql(@"SELECT COUNT(*) +FROM `Herb` AS `h` +WHERE MATCH (`h`.`Name`, `h`.`Garden`) AGAINST ('First Herb') > 0.0"); + } + + [ConditionalFact] + public virtual void IsMatch_multiple_keywords_multiple_columns() + { + using var context = CreateContext(); + var count = context.Set().Count(herb => EF.Functions.IsMatch(new []{herb.Name, herb.Garden}, "First Herb")); Assert.Equal(9, count); AssertSql(@"SELECT COUNT(*) FROM `Herb` AS `h` -WHERE MATCH (`h`.`Name`, `h`.`Garden`) AGAINST ('First Herb')"); +WHERE MATCH (`h`.`Name`, `h`.`Garden`) AGAINST ('First Herb') > 0.0"); } [ConditionalFact] public virtual void Match_multiple_keywords_separated() { using var context = CreateContext(); - var count = context.Set().Count(herb => EF.Functions.Match(herb.Name, "First, Second")); + var count = context.Set().Count(herb => EF.Functions.Match(herb.Name, "First, Second") > 0); + + Assert.Equal(6, count); + + AssertSql(@"SELECT COUNT(*) +FROM `Herb` AS `h` +WHERE MATCH (`h`.`Name`) AGAINST ('First, Second') > 0.0"); + } + + [ConditionalFact] + public virtual void IsMatch_multiple_keywords_separated() + { + using var context = CreateContext(); + var count = context.Set().Count(herb => EF.Functions.IsMatch(herb.Name, "First, Second")); Assert.Equal(6, count); AssertSql(@"SELECT COUNT(*) FROM `Herb` AS `h` -WHERE MATCH (`h`.`Name`) AGAINST ('First, Second')"); +WHERE MATCH (`h`.`Name`) AGAINST ('First, Second') > 0.0"); } [ConditionalFact] public virtual void Match_multiple_keywords_separated_multiple_columns() { using var context = CreateContext(); - var count = context.Set().Count(herb => EF.Functions.Match(new []{herb.Name, herb.Garden}, "First, Second")); + var count = context.Set().Count(herb => EF.Functions.Match(new []{herb.Name, herb.Garden}, "First, Second") > 0); Assert.Equal(8, count); AssertSql(@"SELECT COUNT(*) FROM `Herb` AS `h` -WHERE MATCH (`h`.`Name`, `h`.`Garden`) AGAINST ('First, Second')"); +WHERE MATCH (`h`.`Name`, `h`.`Garden`) AGAINST ('First, Second') > 0.0"); + } + + [ConditionalFact] + public virtual void IsMatch_multiple_keywords_separated_multiple_columns() + { + using var context = CreateContext(); + var count = context.Set().Count(herb => EF.Functions.IsMatch(new []{herb.Name, herb.Garden}, "First, Second")); + + Assert.Equal(8, count); + + AssertSql(@"SELECT COUNT(*) +FROM `Herb` AS `h` +WHERE MATCH (`h`.`Name`, `h`.`Garden`) AGAINST ('First, Second') > 0.0"); } [ConditionalFact] public virtual void Match_with_wildcard() { using var context = CreateContext(); - var count = context.Set().Count(herb => EF.Functions.Match(herb.Name, "First*")); + var count = context.Set().Count(herb => EF.Functions.Match(herb.Name, "First*") > 0); Assert.Equal(3, count); AssertSql(@"SELECT COUNT(*) FROM `Herb` AS `h` -WHERE MATCH (`h`.`Name`) AGAINST ('First*')"); +WHERE MATCH (`h`.`Name`) AGAINST ('First*') > 0.0"); + } + + [ConditionalFact] + public virtual void IsMatch_with_wildcard() + { + using var context = CreateContext(); + var count = context.Set().Count(herb => EF.Functions.IsMatch(herb.Name, "First*")); + + Assert.Equal(3, count); + + AssertSql(@"SELECT COUNT(*) +FROM `Herb` AS `h` +WHERE MATCH (`h`.`Name`) AGAINST ('First*') > 0.0"); } [ConditionalFact] public virtual void Match_in_boolean_mode_keywords() { using var context = CreateContext(); - var count = context.Set().Count(herb => EF.Functions.Match(herb.Name, "First* Herb*")); + var count = context.Set().Count(herb => EF.Functions.Match(herb.Name, "First* Herb*") > 0); Assert.Equal(9, count); AssertSql(@"SELECT COUNT(*) FROM `Herb` AS `h` -WHERE MATCH (`h`.`Name`) AGAINST ('First* Herb*')"); +WHERE MATCH (`h`.`Name`) AGAINST ('First* Herb*') > 0.0"); + } + + [ConditionalFact] + public virtual void IsMatch_in_boolean_mode_keywords() + { + using var context = CreateContext(); + var count = context.Set().Count(herb => EF.Functions.IsMatch(herb.Name, "First* Herb*")); + + Assert.Equal(9, count); + + AssertSql(@"SELECT COUNT(*) +FROM `Herb` AS `h` +WHERE MATCH (`h`.`Name`) AGAINST ('First* Herb*') > 0.0"); } [ConditionalFact] public virtual void Match_keywords_multiple_columns() { using var context = CreateContext(); - var count = context.Set().Count(herb => EF.Functions.Match(new []{herb.Name, herb.Garden}, "First* Herb*")); + var count = context.Set().Count(herb => EF.Functions.Match(new []{herb.Name, herb.Garden}, "First* Herb*") > 0); + + Assert.Equal(9, count); + + AssertSql(@"SELECT COUNT(*) +FROM `Herb` AS `h` +WHERE MATCH (`h`.`Name`, `h`.`Garden`) AGAINST ('First* Herb*') > 0.0"); + } + + [ConditionalFact] + public virtual void IsMatch_keywords_multiple_columns() + { + using var context = CreateContext(); + var count = context.Set().Count(herb => EF.Functions.IsMatch(new []{herb.Name, herb.Garden}, "First* Herb*")); Assert.Equal(9, count); AssertSql(@"SELECT COUNT(*) FROM `Herb` AS `h` -WHERE MATCH (`h`.`Name`, `h`.`Garden`) AGAINST ('First* Herb*')"); +WHERE MATCH (`h`.`Name`, `h`.`Garden`) AGAINST ('First* Herb*') > 0.0"); } [ConditionalFact] public virtual void Match_keyword_excluded() { using var context = CreateContext(); - var count = context.Set().Count(herb => EF.Functions.Match(herb.Name, "Herb* -Second")); + var count = context.Set().Count(herb => EF.Functions.Match(herb.Name, "Herb* -Second") > 0); + + Assert.Equal(6, count); + + AssertSql(@"SELECT COUNT(*) +FROM `Herb` AS `h` +WHERE MATCH (`h`.`Name`) AGAINST ('Herb* -Second') > 0.0"); + } + + [ConditionalFact] + public virtual void IsMatch_keyword_excluded() + { + using var context = CreateContext(); + var count = context.Set().Count(herb => EF.Functions.IsMatch(herb.Name, "Herb* -Second")); Assert.Equal(6, count); AssertSql(@"SELECT COUNT(*) FROM `Herb` AS `h` -WHERE MATCH (`h`.`Name`) AGAINST ('Herb* -Second')"); +WHERE MATCH (`h`.`Name`) AGAINST ('Herb* -Second') > 0.0"); } [ConditionalFact] public virtual void Match_keyword_excluded_multiple_columns() { using var context = CreateContext(); - var count = context.Set().Count(herb => EF.Functions.Match(new []{herb.Name, herb.Garden}, "Herb* -Second")); + var count = context.Set().Count(herb => EF.Functions.Match(new []{herb.Name, herb.Garden}, "Herb* -Second") > 0); + + Assert.Equal(4, count); + + AssertSql(@"SELECT COUNT(*) +FROM `Herb` AS `h` +WHERE MATCH (`h`.`Name`, `h`.`Garden`) AGAINST ('Herb* -Second') > 0.0"); + } + + [ConditionalFact] + public virtual void IsMatch_keyword_excluded_multiple_columns() + { + using var context = CreateContext(); + var count = context.Set().Count(herb => EF.Functions.IsMatch(new []{herb.Name, herb.Garden}, "Herb* -Second")); Assert.Equal(4, count); AssertSql(@"SELECT COUNT(*) FROM `Herb` AS `h` -WHERE MATCH (`h`.`Name`, `h`.`Garden`) AGAINST ('Herb* -Second')"); +WHERE MATCH (`h`.`Name`, `h`.`Garden`) AGAINST ('Herb* -Second') > 0.0"); } private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySingleStoreTest.cs new file mode 100644 index 000000000..0ee7e9200 --- /dev/null +++ b/test/EFCore.SingleStore.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySingleStoreTest.cs @@ -0,0 +1,503 @@ +using System; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.TestUtilities; +using EntityFrameworkCore.SingleStore.FunctionalTests.TestUtilities; +using EntityFrameworkCore.SingleStore.Tests; +using Xunit; + +namespace EntityFrameworkCore.SingleStore.FunctionalTests.Query; + +public class NonSharedPrimitiveCollectionsQuerySingleStoreTest : NonSharedPrimitiveCollectionsQueryRelationalTestBase +{ + #region Support for specific element types + + public override async Task Array_of_string() + { + await base.Array_of_string(); + + AssertSql( +""" +SELECT `t`.`Id`, `t`.`Ints`, `t`.`SomeArray` +FROM `TestEntity` AS `t` +WHERE ( + SELECT COUNT(*) + FROM JSON_TABLE(`t`.`SomeArray`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` longtext PATH '$[0]' + )) AS `s` + WHERE `s`.`value` = 'a') = 2 +LIMIT 2 +"""); + } + + public override async Task Array_of_int() + { + await base.Array_of_int(); + + AssertSql( +""" +SELECT `t`.`Id`, `t`.`Ints`, `t`.`SomeArray` +FROM `TestEntity` AS `t` +WHERE ( + SELECT COUNT(*) + FROM JSON_TABLE(`t`.`SomeArray`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `s` + WHERE `s`.`value` = 1) = 2 +LIMIT 2 +"""); + } + + public override async Task Array_of_long() + { + await base.Array_of_long(); + + AssertSql( +""" +SELECT `t`.`Id`, `t`.`Ints`, `t`.`SomeArray` +FROM `TestEntity` AS `t` +WHERE ( + SELECT COUNT(*) + FROM JSON_TABLE(`t`.`SomeArray`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` bigint PATH '$[0]' + )) AS `s` + WHERE `s`.`value` = 1) = 2 +LIMIT 2 +"""); + } + + public override async Task Array_of_short() + { + await base.Array_of_short(); + + AssertSql( +""" +SELECT `t`.`Id`, `t`.`Ints`, `t`.`SomeArray` +FROM `TestEntity` AS `t` +WHERE ( + SELECT COUNT(*) + FROM JSON_TABLE(`t`.`SomeArray`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` smallint PATH '$[0]' + )) AS `s` + WHERE `s`.`value` = 1) = 2 +LIMIT 2 +"""); + } + + [ConditionalFact] + public override Task Array_of_byte() + => base.Array_of_byte(); + + public override async Task Array_of_double() + { + await base.Array_of_double(); + + AssertSql( +""" +SELECT `t`.`Id`, `t`.`Ints`, `t`.`SomeArray` +FROM `TestEntity` AS `t` +WHERE ( + SELECT COUNT(*) + FROM JSON_TABLE(`t`.`SomeArray`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` double PATH '$[0]' + )) AS `s` + WHERE `s`.`value` = 1.0) = 2 +LIMIT 2 +"""); + } + + public override async Task Array_of_float() + { + await base.Array_of_float(); + + AssertSql( +""" +SELECT `t`.`Id`, `t`.`Ints`, `t`.`SomeArray` +FROM `TestEntity` AS `t` +WHERE ( + SELECT COUNT(*) + FROM JSON_TABLE(`t`.`SomeArray`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` float PATH '$[0]' + )) AS `s` + WHERE `s`.`value` = 1) = 2 +LIMIT 2 +"""); + } + + public override async Task Array_of_decimal() + { + await base.Array_of_decimal(); + + AssertSql( +""" +SELECT `t`.`Id`, `t`.`Ints`, `t`.`SomeArray` +FROM `TestEntity` AS `t` +WHERE ( + SELECT COUNT(*) + FROM JSON_TABLE(`t`.`SomeArray`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` decimal(65,30) PATH '$[0]' + )) AS `s` + WHERE `s`.`value` = 1.0) = 2 +LIMIT 2 +"""); + } + + public override async Task Array_of_DateTime() + { + await base.Array_of_DateTime(); + + AssertSql( +""" +SELECT `t`.`Id`, `t`.`Ints`, `t`.`SomeArray` +FROM `TestEntity` AS `t` +WHERE ( + SELECT COUNT(*) + FROM JSON_TABLE(`t`.`SomeArray`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` datetime(6) PATH '$[0]' + )) AS `s` + WHERE `s`.`value` = TIMESTAMP '2023-01-01 12:30:00') = 2 +LIMIT 2 +"""); + } + + public override async Task Array_of_DateTime_with_milliseconds() + { + await base.Array_of_DateTime_with_milliseconds(); + + AssertSql( +""" +SELECT `t`.`Id`, `t`.`Ints`, `t`.`SomeArray` +FROM `TestEntity` AS `t` +WHERE ( + SELECT COUNT(*) + FROM JSON_TABLE(`t`.`SomeArray`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` datetime(6) PATH '$[0]' + )) AS `s` + WHERE `s`.`value` = TIMESTAMP '2023-01-01 12:30:00.123') = 2 +LIMIT 2 +"""); + } + + public override async Task Array_of_DateTime_with_microseconds() + { + await base.Array_of_DateTime_with_microseconds(); + + AssertSql( +""" +SELECT `t`.`Id`, `t`.`Ints`, `t`.`SomeArray` +FROM `TestEntity` AS `t` +WHERE ( + SELECT COUNT(*) + FROM JSON_TABLE(`t`.`SomeArray`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` datetime(6) PATH '$[0]' + )) AS `s` + WHERE `s`.`value` = TIMESTAMP '2023-01-01 12:30:00.123456') = 2 +LIMIT 2 +"""); + } + + public override async Task Array_of_DateOnly() + { + await base.Array_of_DateOnly(); + + AssertSql( +""" +SELECT `t`.`Id`, `t`.`Ints`, `t`.`SomeArray` +FROM `TestEntity` AS `t` +WHERE ( + SELECT COUNT(*) + FROM JSON_TABLE(`t`.`SomeArray`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` date PATH '$[0]' + )) AS `s` + WHERE `s`.`value` = DATE '2023-01-01') = 2 +LIMIT 2 +"""); + } + + public override async Task Array_of_TimeOnly() + { + await base.Array_of_TimeOnly(); + + AssertSql( +""" +SELECT `t`.`Id`, `t`.`Ints`, `t`.`SomeArray` +FROM `TestEntity` AS `t` +WHERE ( + SELECT COUNT(*) + FROM JSON_TABLE(`t`.`SomeArray`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` time(6) PATH '$[0]' + )) AS `s` + WHERE `s`.`value` = TIME '12:30:00') = 2 +LIMIT 2 +"""); + } + + public override async Task Array_of_TimeOnly_with_milliseconds() + { + await base.Array_of_TimeOnly_with_milliseconds(); + + AssertSql( +""" +SELECT `t`.`Id`, `t`.`Ints`, `t`.`SomeArray` +FROM `TestEntity` AS `t` +WHERE ( + SELECT COUNT(*) + FROM JSON_TABLE(`t`.`SomeArray`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` time(6) PATH '$[0]' + )) AS `s` + WHERE `s`.`value` = TIME '12:30:00.123') = 2 +LIMIT 2 +"""); + } + + public override async Task Array_of_TimeOnly_with_microseconds() + { + await base.Array_of_TimeOnly_with_microseconds(); + + AssertSql( +""" +SELECT `t`.`Id`, `t`.`Ints`, `t`.`SomeArray` +FROM `TestEntity` AS `t` +WHERE ( + SELECT COUNT(*) + FROM JSON_TABLE(`t`.`SomeArray`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` time(6) PATH '$[0]' + )) AS `s` + WHERE `s`.`value` = TIME '12:30:00.123456') = 2 +LIMIT 2 +"""); + } + + public override async Task Array_of_DateTimeOffset() + { + await base.Array_of_DateTimeOffset(); + + AssertSql( +""" +SELECT `t`.`Id`, `t`.`Ints`, `t`.`SomeArray` +FROM `TestEntity` AS `t` +WHERE ( + SELECT COUNT(*) + FROM JSON_TABLE(`t`.`SomeArray`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` datetime(6) PATH '$[0]' + )) AS `s` + WHERE `s`.`value` = TIMESTAMP '2023-01-01 10:30:00') = 2 +LIMIT 2 +"""); + } + + public override async Task Array_of_bool() + { + await base.Array_of_bool(); + + AssertSql( +""" +SELECT `t`.`Id`, `t`.`Ints`, `t`.`SomeArray` +FROM `TestEntity` AS `t` +WHERE ( + SELECT COUNT(*) + FROM JSON_TABLE(`t`.`SomeArray`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` tinyint(1) PATH '$[0]' + )) AS `s` + WHERE `s`.`value`) = 2 +LIMIT 2 +"""); + } + + public override async Task Array_of_Guid() + { + await base.Array_of_Guid(); + + AssertSql( +""" +SELECT `t`.`Id`, `t`.`Ints`, `t`.`SomeArray` +FROM `TestEntity` AS `t` +WHERE ( + SELECT COUNT(*) + FROM JSON_TABLE(`t`.`SomeArray`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` char(36) PATH '$[0]' + )) AS `s` + WHERE `s`.`value` = 'dc8c903d-d655-4144-a0fd-358099d40ae1') = 2 +LIMIT 2 +"""); + } + + public override async Task Array_of_byte_array() + { + // This does not work, because the byte array is base64 encoded for some reason. + await base.Array_of_byte_array(); + + AssertSql( +""" +SELECT `t`.`Id`, `t`.`Ints`, `t`.`SomeArray` +FROM `TestEntity` AS `t` +WHERE ( + SELECT COUNT(*) + FROM JSON_TABLE(`t`.`SomeArray`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` longblob PATH '$[0]' + )) AS `s` + WHERE FROM_BASE64(`s`.`value`) = 0x0102) = 2 +LIMIT 2 +"""); + } + + public override async Task Array_of_enum() + { + await base.Array_of_enum(); + + AssertSql( +""" +SELECT `t`.`Id`, `t`.`Ints`, `t`.`SomeArray` +FROM `TestEntity` AS `t` +WHERE ( + SELECT COUNT(*) + FROM JSON_TABLE(`t`.`SomeArray`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `s` + WHERE `s`.`value` = 0) = 2 +LIMIT 2 +"""); + } + + [ConditionalFact] + public override Task Array_of_array_is_not_supported() + => base.Array_of_array_is_not_supported(); + + [ConditionalFact] + public override Task Multidimensional_array_is_not_supported() + => base.Multidimensional_array_is_not_supported(); + + #endregion Support for specific element types + + [ConditionalFact] + public override Task Column_with_custom_converter() + => base.Column_with_custom_converter(); + + public override async Task Parameter_with_inferred_value_converter() + { + await base.Parameter_with_inferred_value_converter(); + + AssertSql(""); + } + + #region Type mapping inference + + public override async Task Constant_with_inferred_value_converter() + { + await base.Constant_with_inferred_value_converter(); + + if (AppConfig.ServerVersion.Supports.ValuesWithRows) + { + AssertSql( +""" +SELECT `t`.`Id`, `t`.`Ints`, `t`.`PropertyWithValueConverter` +FROM `TestEntity` AS `t` +WHERE ( + SELECT COUNT(*) + FROM (SELECT CAST(1 AS signed) AS `Value` UNION ALL VALUES ROW(8)) AS `v` + WHERE `v`.`Value` = `t`.`PropertyWithValueConverter`) = 1 +LIMIT 2 +"""); + } + else if (AppConfig.ServerVersion.Supports.Values) + { + + } + else + { + AssertSql( + """ + SELECT `t`.`Id`, `t`.`Ints`, `t`.`PropertyWithValueConverter` + FROM `TestEntity` AS `t` + WHERE ( + SELECT COUNT(*) + FROM (SELECT CAST(1 AS signed) AS `Value` UNION ALL SELECT 8) AS `v` + WHERE `v`.`Value` = `t`.`PropertyWithValueConverter`) = 1 + LIMIT 2 + """); + } + } + + public override async Task Inline_collection_in_query_filter() + { + await base.Inline_collection_in_query_filter(); + + if (AppConfig.ServerVersion.Supports.ValuesWithRows) + { + AssertSql( +""" +SELECT `t`.`Id`, `t`.`Ints` +FROM `TestEntity` AS `t` +WHERE ( + SELECT COUNT(*) + FROM (SELECT CAST(1 AS signed) AS `Value` UNION ALL VALUES ROW(2), ROW(3)) AS `v` + WHERE `v`.`Value` > `t`.`Id`) = 1 +LIMIT 2 +"""); + } + else if (AppConfig.ServerVersion.Supports.Values) + { + } + else + { + AssertSql( +""" +SELECT `t`.`Id`, `t`.`Ints` +FROM `TestEntity` AS `t` +WHERE ( + SELECT COUNT(*) + FROM (SELECT CAST(1 AS signed) AS `Value` UNION ALL SELECT 2 UNION ALL SELECT 3) AS `v` + WHERE `v`.`Value` > `t`.`Id`) = 1 +LIMIT 2 +"""); + } + } + + public override async Task Column_collection_inside_json_owned_entity() + { + await base.Column_collection_inside_json_owned_entity(); + + AssertSql( + """ +SELECT TOP(2) [t].[Id], [t].[Owned] +FROM [TestOwner] AS [t] +WHERE ( + SELECT COUNT(*) + FROM OPENJSON(JSON_VALUE([t].[Owned], '$.Strings')) AS [s]) = 2 +""", + // + """ +SELECT TOP(2) [t].[Id], [t].[Owned] +FROM [TestOwner] AS [t] +WHERE JSON_VALUE(JSON_VALUE([t].[Owned], '$.Strings'), '$[1]') = N'bar' +"""); + } + + #endregion Type mapping inference + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + protected override ITestStoreFactory TestStoreFactory + => SingleStoreTestStoreFactory.Instance; +} diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySingleStoreTest.cs index 7a4e34586..9a67ff55e 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySingleStoreTest.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; using System.Linq; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.TestModels.Northwind; @@ -36,16 +37,34 @@ public override Task Average_over_nested_subquery_is_client_eval(bool async) selector: c => (decimal)c.Orders.Average(o => 5 + o.OrderDetails.Average(od => od.ProductID)), asserter: (a, b) => Assert.Equal(a, b, 12)); // added flouting point precision tolerance + // TODO: Implement TranslatePrimitiveCollection. public override async Task Contains_with_local_anonymous_type_array_closure(bool async) { // Aggregates. Issue #15937. - await AssertTranslationFailed(() => base.Contains_with_local_anonymous_type_array_closure(async)); + // await AssertTranslationFailed(() => base.Contains_with_local_anonymous_type_array_closure(async)); + + await Assert.ThrowsAsync(() => base.Contains_with_local_anonymous_type_array_closure(async)); AssertSql(); } + // TODO: Implement TranslatePrimitiveCollection public override async Task Contains_with_local_tuple_array_closure(bool async) - => await AssertTranslationFailed(() => base.Contains_with_local_tuple_array_closure(async)); + { + // await AssertTranslationFailed(() => base.Contains_with_local_tuple_array_closure(async)); + + await Assert.ThrowsAsync(() => base.Contains_with_local_tuple_array_closure(async)); + } + + public override async Task Contains_with_local_enumerable_inline(bool async) + { + // Issue #31776 + await Assert.ThrowsAsync( + async () => + await base.Contains_with_local_enumerable_inline(async)); + + AssertSql(); + } [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] public override Task Collection_Last_member_access_in_projection_translated(bool async) diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindAggregateQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindAggregateQuerySingleStoreTest.cs deleted file mode 100644 index df6b447fd..000000000 --- a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindAggregateQuerySingleStoreTest.cs +++ /dev/null @@ -1,200 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore.Query; -using Microsoft.EntityFrameworkCore.TestModels.Northwind; -using Microsoft.EntityFrameworkCore.TestUtilities; -using EntityFrameworkCore.SingleStore.FunctionalTests.TestUtilities; -using Xunit; -using Xunit.Abstractions; - -namespace EntityFrameworkCore.SingleStore.FunctionalTests.Query -{ - public class NorthwindAggregateQuerySingleStoreTest : NorthwindAggregateOperatorsQueryRelationalTestBase< - NorthwindQuerySingleStoreFixture> - { - public NorthwindAggregateQuerySingleStoreTest( - NorthwindQuerySingleStoreFixture fixture, - ITestOutputHelper testOutputHelper) - : base(fixture) - { - ClearLog(); - //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] - public override Task Collection_Last_member_access_in_projection_translated(bool async) - { - return base.Collection_Last_member_access_in_projection_translated(async); - } - - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] - public override Task Collection_LastOrDefault_member_access_in_projection_translated(bool async) - { - return base.Collection_LastOrDefault_member_access_in_projection_translated(async); - } - - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] - public override Task First_inside_subquery_gets_client_evaluated(bool async) - { - return base.First_inside_subquery_gets_client_evaluated(async); - } - - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] - public override Task FirstOrDefault_inside_subquery_gets_server_evaluated(bool async) - { - return base.FirstOrDefault_inside_subquery_gets_server_evaluated(async); - } - - [ConditionalTheory(Skip = "SingleStore does not support this type of query: scalar subselect references field belonging to outer select that is more than one level up")] - public override Task Multiple_collection_navigation_with_FirstOrDefault_chained_projecting_scalar(bool async) - { - return base.Multiple_collection_navigation_with_FirstOrDefault_chained_projecting_scalar(async); - } - - [ConditionalTheory(Skip = "Feature 'Subselect in aggregate functions' is not supported by SingleStore")] - public override async Task Average_over_max_subquery_is_client_eval(bool async) - { - await AssertAverage( - async, - ss => ss.Set().OrderBy(c => c.CustomerID).Take(3), - selector: c => (decimal)c.Orders.Average(o => 5 + o.OrderDetails.Max(od => od.ProductID)), - asserter: (a, b) => Assert.Equal(a, b, 12)); // added flouting point precision tolerance - - AssertSql( - $@"@__p_0='3' -SELECT AVG(CAST(( - SELECT AVG({SingleStoreTestHelpers.CastAsDouble(@"5 + ( - SELECT MAX(`o0`.`ProductID`) - FROM `Order Details` AS `o0` - WHERE `o`.`OrderID` = `o0`.`OrderID`)")}) - FROM `Orders` AS `o` - WHERE `t`.`CustomerID` = `o`.`CustomerID`) AS decimal(65,30))) -FROM ( - SELECT `c`.`CustomerID` - FROM `Customers` AS `c` - ORDER BY `c`.`CustomerID` - LIMIT @__p_0 -) AS `t`"); - } - - [ConditionalTheory(Skip = "Feature 'Subselect in aggregate functions' is not supported by SingleStore")] - public override async Task Average_over_nested_subquery_is_client_eval(bool async) - { - await AssertAverage( - async, - ss => ss.Set().OrderBy(c => c.CustomerID).Take(3), - selector: c => (decimal)c.Orders.Average(o => 5 + o.OrderDetails.Average(od => od.ProductID)), - asserter: (a, b) => Assert.Equal(a, b, 12)); // added flouting point precision tolerance - - AssertSql( - $@"@__p_0='3' -SELECT AVG(CAST(( - SELECT AVG(5.0 + ( - SELECT AVG({SingleStoreTestHelpers.CastAsDouble(@"`o0`.`ProductID`")}) - FROM `Order Details` AS `o0` - WHERE `o`.`OrderID` = `o0`.`OrderID`)) - FROM `Orders` AS `o` - WHERE `t`.`CustomerID` = `o`.`CustomerID`) AS decimal(65,30))) -FROM ( - SELECT `c`.`CustomerID` - FROM `Customers` AS `c` - ORDER BY `c`.`CustomerID` - LIMIT @__p_0 -) AS `t`"); - } - - [ConditionalTheory(Skip = "Feature 'Subselect in aggregate functions' is not supported by SingleStore")] - public override Task Average_over_subquery_is_client_eval(bool async) - { - return base.Average_over_max_subquery_is_client_eval(async); - } - - [ConditionalTheory(Skip = "Feature 'Subselect in aggregate functions' is not supported by SingleStore")] - public override Task Max_over_nested_subquery_is_client_eval(bool async) - { - return base.Max_over_nested_subquery_is_client_eval(async); - } - - [ConditionalTheory(Skip = "Feature 'Subselect in aggregate functions' is not supported by SingleStore")] - public override Task Max_over_subquery_is_client_eval(bool async) - { - return base.Max_over_subquery_is_client_eval(async); - } - - [ConditionalTheory(Skip = "Feature 'Subselect in aggregate functions' is not supported by SingleStore")] - public override Task Max_over_sum_subquery_is_client_eval(bool async) - { - return base.Max_over_sum_subquery_is_client_eval(async); - } - - [ConditionalTheory(Skip = "Feature 'Subselect in aggregate functions' is not supported by SingleStore")] - public override Task Min_over_max_subquery_is_client_eval(bool async) - { - return base.Min_over_max_subquery_is_client_eval(async); - } - - [ConditionalTheory(Skip = "Feature 'Subselect in aggregate functions' is not supported by SingleStore")] - public override Task Min_over_nested_subquery_is_client_eval(bool async) - { - return base.Min_over_nested_subquery_is_client_eval(async); - } - - [ConditionalTheory(Skip = "Feature 'Subselect in aggregate functions' is not supported by SingleStore")] - public override Task Min_over_subquery_is_client_eval(bool async) - { - return base.Min_over_subquery_is_client_eval(async); - } - - [ConditionalTheory(Skip = "Feature 'Subselect in aggregate functions' is not supported by SingleStore")] - public override Task Sum_on_float_column_in_subquery(bool async) - { - return base.Sum_on_float_column_in_subquery(async); - } - - [ConditionalTheory(Skip = "Feature 'Subselect in aggregate functions' is not supported by SingleStore")] - public override Task Sum_over_min_subquery_is_client_eval(bool async) - { - return base.Sum_over_min_subquery_is_client_eval(async); - } - - [ConditionalTheory(Skip = "Feature 'Subselect in aggregate functions' is not supported by SingleStore")] - public override Task Sum_over_nested_subquery_is_client_eval(bool async) - { - return base.Sum_over_nested_subquery_is_client_eval(async); - } - - [ConditionalTheory(Skip = "Feature 'Subselect in aggregate functions' is not supported by SingleStore")] - public override Task Sum_over_subquery_is_client_eval(bool async) - { - return base.Sum_over_subquery_is_client_eval(async); - } - - public override async Task Contains_with_local_anonymous_type_array_closure(bool async) - { - // Aggregates. Issue #15937. - await AssertTranslationFailed(() => base.Contains_with_local_anonymous_type_array_closure(async)); - - AssertSql(); - } - - public override async Task Contains_with_local_tuple_array_closure(bool async) - => await AssertTranslationFailed(() => base.Contains_with_local_tuple_array_closure(async)); - - public override async Task Sum_with_coalesce(bool async) - { - await base.Sum_with_coalesce(async); - - AssertSql( - @"SELECT COALESCE(SUM(COALESCE(`p`.`UnitPrice`, 0.0)), 0.0) -FROM `Products` AS `p` -WHERE `p`.`ProductID` < 40"); - } - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - - protected override void ClearLog() - => Fixture.TestSqlLoggerFactory.Clear(); - } -} diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindChangeTrackingQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindChangeTrackingQuerySingleStoreTest.cs index ee49c3c79..53b98b506 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindChangeTrackingQuerySingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindChangeTrackingQuerySingleStoreTest.cs @@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.TestModels.Northwind; using Microsoft.EntityFrameworkCore.TestUtilities; +using EntityFrameworkCore.SingleStore.FunctionalTests.TestModels.Northwind; using Xunit; namespace EntityFrameworkCore.SingleStore.FunctionalTests.Query @@ -47,7 +48,7 @@ public override void Multiple_entities_can_revert() } protected override NorthwindContext CreateNoTrackingContext() - => new NorthwindRelationalContext( + => new NorthwindSingleStoreContext( new DbContextOptionsBuilder(Fixture.CreateOptions()) .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking).Options); } diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindCompiledQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindCompiledQuerySingleStoreTest.cs index d18dfd049..85c1032ae 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindCompiledQuerySingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindCompiledQuerySingleStoreTest.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Query; @@ -23,68 +22,6 @@ public NorthwindCompiledQuerySingleStoreTest( //fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } - [ConditionalFact] - public override void Query_with_array_parameter() - { - var query = EF.CompileQuery( - (NorthwindContext context, string[] args) - => context.Customers.Where(c => c.CustomerID == args[0])); - - using (var context = CreateContext()) - { - // We still throw an InvalidOperationException, which is expected, but with a different error message, - // because of the implemented array support for JSON. - Assert.Throws( - () => query(context, new[] {"ALFKI"}) - .First() - .CustomerID); - } - - using (var context = CreateContext()) - { - // We still throw an InvalidOperationException, which is expected, but with a different error message, - // because of the implemented array support for JSON. - Assert.Throws( - () => query(context, new[] {"ANATR"}) - .First() - .CustomerID); - } - } - - [ConditionalFact] - public override async Task Query_with_array_parameter_async() - { - var query = EF.CompileAsyncQuery( - (NorthwindContext context, string[] args) - => context.Customers.Where(c => c.CustomerID == args[0])); - - using (var context = CreateContext()) - { - // We still throw an InvalidOperationException, which is expected, but with a different error message, - // because of the implemented array support for JSON. - await Assert.ThrowsAsync( - () => query(context, new[] {"ALFKI"}) - .ToListAsync()); - } - - using (var context = CreateContext()) - { - // We still throw an InvalidOperationException, which is expected, but with a different error message, - // because of the implemented array support for JSON. - await Assert.ThrowsAsync( - () => query(context, new[] {"ANATR"}) - .ToListAsync()); - } - } - - public override void MakeBinary_does_not_throw_for_unsupported_operator() - { - Assert.Equal( - CoreStrings.TranslationFailed("DbSet() .Where(c => c.CustomerID == (string)(__parameters[0]))"), - Assert.Throws( - () => base.MakeBinary_does_not_throw_for_unsupported_operator()).Message.Replace("\r", "").Replace("\n", "")); - } - private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindDbFunctionsQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindDbFunctionsQuerySingleStoreTest.cs index 80363c0d9..41cd66e4d 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindDbFunctionsQuerySingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindDbFunctionsQuerySingleStoreTest.cs @@ -1,6 +1,11 @@ +using System; +using System.Linq.Expressions; using System.Threading.Tasks; +using EntityFrameworkCore.SingleStore.Tests; +using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.TestUtilities; +using Microsoft.EntityFrameworkCore.TestModels.Northwind; using Xunit; using Xunit.Abstractions; @@ -20,7 +25,25 @@ public NorthwindDbFunctionsQuerySingleStoreTest( public override async Task Like_literal(bool async) { - await base.Like_literal(async); + Expression> expectedPredicate; + if (AppConfig.ServerVersion.Version.Major >= 9) + { + // starting from 9.0 SingleStore's standard collation is utf8mb4_bin + // it is case‑sensitive, so only uppercase M will match the LIKE pattern + expectedPredicate = c => c.ContactName.Contains("M"); + } + else + { + // older collations are case‑insensitive + expectedPredicate = c => c.ContactName.Contains("M") || c.ContactName.Contains("m"); + } + + await AssertCount( + async, + ss => ss.Set(), + ss => ss.Set(), + c => EF.Functions.Like(c.ContactName, "%M%"), + expectedPredicate); AssertSql( @"SELECT COUNT(*) diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindEFPropertyIncludeQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindEFPropertyIncludeQuerySingleStoreTest.cs index c65151532..b52dc5a60 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindEFPropertyIncludeQuerySingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindEFPropertyIncludeQuerySingleStoreTest.cs @@ -6,6 +6,9 @@ using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.TestModels.Northwind; using Microsoft.EntityFrameworkCore.TestUtilities; +using EntityFrameworkCore.SingleStore.FunctionalTests.TestUtilities; +using EntityFrameworkCore.SingleStore.Tests; +using Microsoft.CodeAnalysis.VisualBasic.Syntax; using Xunit; namespace EntityFrameworkCore.SingleStore.FunctionalTests.Query; @@ -148,7 +151,7 @@ public override async Task Include_collection_alias_generation(bool async) SELECT `o`.`OrderID`, `o`.`CustomerID`, `o`.`EmployeeID`, `o`.`OrderDate`, `o0`.`OrderID`, `o0`.`ProductID`, `o0`.`Discount`, `o0`.`Quantity`, `o0`.`UnitPrice` FROM `Orders` AS `o` LEFT JOIN `Order Details` AS `o0` ON `o`.`OrderID` = `o0`.`OrderID` -WHERE `o`.`CustomerID` IS NOT NULL AND (`o`.`CustomerID` LIKE 'F%') +WHERE `o`.`CustomerID` LIKE 'F%' ORDER BY `o`.`OrderID`, `o0`.`OrderID` """); } @@ -249,7 +252,7 @@ public override async Task Include_references_then_include_collection(bool async FROM `Orders` AS `o` LEFT JOIN `Customers` AS `c` ON `o`.`CustomerID` = `c`.`CustomerID` LEFT JOIN `Orders` AS `o0` ON `c`.`CustomerID` = `o0`.`CustomerID` -WHERE `o`.`CustomerID` IS NOT NULL AND (`o`.`CustomerID` LIKE 'F%') +WHERE `o`.`CustomerID` LIKE 'F%' ORDER BY `o`.`OrderID`, `c`.`CustomerID` """); } @@ -415,8 +418,7 @@ from c2 in ss.Set().OrderBy(c => c.CustomerID).Skip(2).Take(2) { AssertInclude(e.c1, a.c1, new ExpectedInclude(c => c.Orders)); AssertEqual(e.c2, a.c2); - }, - entryCount: 8); + }); AssertSql( """ @@ -459,8 +461,7 @@ await AssertQuery( .OrderBy(b => b.Customer.CustomerID != null) .ThenBy(b => b.Customer != null ? b.Customer.CustomerID : string.Empty) .ThenBy(b => b.EmployeeID) // Needs to be explicitly ordered by EmployeeID as well - .Take(2), - entryCount: 6); + .Take(2)); AssertSql( """ @@ -662,21 +663,53 @@ public override async Task Include_collection_OrderBy_list_does_not_contains(boo { await base.Include_collection_OrderBy_list_does_not_contains(async); - AssertSql( -""" -@__p_1='1' - -SELECT `t`.`CustomerID`, `t`.`Address`, `t`.`City`, `t`.`CompanyName`, `t`.`ContactName`, `t`.`ContactTitle`, `t`.`Country`, `t`.`Fax`, `t`.`Phone`, `t`.`PostalCode`, `t`.`Region`, `o`.`OrderID`, `o`.`CustomerID`, `o`.`EmployeeID`, `o`.`OrderDate` -FROM ( - SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region`, `c`.`CustomerID` <> 'ALFKI' AS `c` - FROM `Customers` AS `c` - WHERE `c`.`CustomerID` LIKE 'A%' - ORDER BY `c`.`CustomerID` <> 'ALFKI' - LIMIT 18446744073709551610 OFFSET @__p_1 -) AS `t` -LEFT JOIN `Orders` AS `o` ON `t`.`CustomerID` = `o`.`CustomerID` -ORDER BY `t`.`c`, `t`.`CustomerID` -"""); + if (SingleStoreTestHelpers.HasPrimitiveCollectionsSupport(Fixture)) + { + AssertSql( + """ + @__p_1='1' + SELECT `t`.`CustomerID`, `t`.`Address`, `t`.`City`, `t`.`CompanyName`, `t`.`ContactName`, `t`.`ContactTitle`, `t`.`Country`, `t`.`Fax`, `t`.`Phone`, `t`.`PostalCode`, `t`.`Region`, `o`.`OrderID`, `o`.`CustomerID`, `o`.`EmployeeID`, `o`.`OrderDate` + FROM ( + SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region`, NOT (COALESCE(`c`.`CustomerID` IN ( + SELECT `l`.`value` + FROM JSON_TABLE('["ALFKI"]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` char(5) PATH '$[0]' + )) AS `l` + ), FALSE)) AS `c` + FROM `Customers` AS `c` + WHERE `c`.`CustomerID` LIKE 'A%' + ORDER BY NOT (COALESCE(`c`.`CustomerID` IN ( + SELECT `l`.`value` + FROM JSON_TABLE('["ALFKI"]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` char(5) PATH '$[0]' + )) AS `l` + ), FALSE)) + LIMIT 18446744073709551610 OFFSET @__p_1 + ) AS `t` + LEFT JOIN `Orders` AS `o` ON `t`.`CustomerID` = `o`.`CustomerID` + ORDER BY `t`.`c`, `t`.`CustomerID` + """); + } + else + { + AssertSql( + """ + @__p_1='1' + + SELECT `t`.`CustomerID`, `t`.`Address`, `t`.`City`, `t`.`CompanyName`, `t`.`ContactName`, `t`.`ContactTitle`, `t`.`Country`, `t`.`Fax`, `t`.`Phone`, `t`.`PostalCode`, `t`.`Region`, `o`.`OrderID`, `o`.`CustomerID`, `o`.`EmployeeID`, `o`.`OrderDate` + FROM ( + SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region`, `c`.`CustomerID` <> 'ALFKI' AS `c` + FROM `Customers` AS `c` + WHERE `c`.`CustomerID` LIKE 'A%' + ORDER BY `c`.`CustomerID` <> 'ALFKI' + LIMIT 18446744073709551610 OFFSET @__p_1 + ) AS `t` + LEFT JOIN `Orders` AS `o` ON `t`.`CustomerID` = `o`.`CustomerID` + ORDER BY `t`.`c`, `t`.`CustomerID` + """); + } } public override async Task Include_reference_dependent_already_tracked(bool async) @@ -1007,7 +1040,38 @@ public override async Task Include_collection_OrderBy_empty_list_contains(bool a { await base.Include_collection_OrderBy_empty_list_contains(async); - AssertSql( + if (SingleStoreTestHelpers.HasPrimitiveCollectionsSupport(Fixture)) + { + AssertSql( + """ + @__p_1='1' + SELECT `t`.`CustomerID`, `t`.`Address`, `t`.`City`, `t`.`CompanyName`, `t`.`ContactName`, `t`.`ContactTitle`, `t`.`Country`, `t`.`Fax`, `t`.`Phone`, `t`.`PostalCode`, `t`.`Region`, `o`.`OrderID`, `o`.`CustomerID`, `o`.`EmployeeID`, `o`.`OrderDate` + FROM ( + SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region`, COALESCE(`c`.`CustomerID` IN ( + SELECT `l`.`value` + FROM JSON_TABLE('[]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` char(5) PATH '$[0]' + )) AS `l` + ), FALSE) AS `c` + FROM `Customers` AS `c` + WHERE `c`.`CustomerID` LIKE 'A%' + ORDER BY COALESCE(`c`.`CustomerID` IN ( + SELECT `l`.`value` + FROM JSON_TABLE('[]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` char(5) PATH '$[0]' + )) AS `l` + ), FALSE) + LIMIT 18446744073709551610 OFFSET @__p_1 + ) AS `t` + LEFT JOIN `Orders` AS `o` ON `t`.`CustomerID` = `o`.`CustomerID` + ORDER BY `t`.`c`, `t`.`CustomerID` + """); + } + else + { + AssertSql( """ @__p_1='1' @@ -1022,6 +1086,7 @@ LIMIT 18446744073709551610 OFFSET @__p_1 LEFT JOIN `Orders` AS `o` ON `t`.`CustomerID` = `o`.`CustomerID` ORDER BY `t`.`c`, `t`.`CustomerID` """); + } } public override async Task Include_references_and_collection_multi_level(bool async) @@ -1141,8 +1206,7 @@ await AssertQuery( .ThenBy(o => o.Customer != null ? o.Customer.City : string.Empty) .ThenBy(o => o.OrderID) // Needs to be explicitly ordered by EmployeeID as well .Take(5), - elementAsserter: (e, a) => AssertInclude(e, a, new ExpectedInclude(o => o.OrderDetails)), - entryCount: 14); + elementAsserter: (e, a) => AssertInclude(e, a, new ExpectedInclude(o => o.OrderDetails))); AssertSql( """ @@ -1176,7 +1240,7 @@ public override async Task Include_reference_when_entity_in_projection(bool asyn SELECT `o`.`OrderID`, `o`.`CustomerID`, `o`.`EmployeeID`, `o`.`OrderDate`, `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Orders` AS `o` LEFT JOIN `Customers` AS `c` ON `o`.`CustomerID` = `c`.`CustomerID` -WHERE `o`.`CustomerID` IS NOT NULL AND (`o`.`CustomerID` LIKE 'F%') +WHERE `o`.`CustomerID` LIKE 'F%' """); } @@ -1322,21 +1386,6 @@ public override async Task Include_multiple_references_then_include_collection_m """); } - public override async Task Outer_idenfier_correctly_determined_when_doing_include_on_right_side_of_left_join(bool async) - { - await base.Outer_idenfier_correctly_determined_when_doing_include_on_right_side_of_left_join(async); - - AssertSql( -""" -SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region`, `o`.`OrderID`, `o`.`CustomerID`, `o`.`EmployeeID`, `o`.`OrderDate`, `o0`.`OrderID`, `o0`.`ProductID`, `o0`.`Discount`, `o0`.`Quantity`, `o0`.`UnitPrice` -FROM `Customers` AS `c` -LEFT JOIN `Orders` AS `o` ON `c`.`CustomerID` = `o`.`CustomerID` -LEFT JOIN `Order Details` AS `o0` ON `o`.`OrderID` = `o0`.`OrderID` -WHERE `c`.`City` = 'Seattle' -ORDER BY `c`.`CustomerID`, `o`.`OrderID`, `o0`.`OrderID` -"""); - } - public override async Task SelectMany_Include_reference_GroupBy_Select(bool async) { await base.SelectMany_Include_reference_GroupBy_Select(async); @@ -1399,21 +1448,53 @@ public override async Task Include_collection_OrderBy_list_contains(bool async) { await base.Include_collection_OrderBy_list_contains(async); - AssertSql( -""" -@__p_1='1' - -SELECT `t`.`CustomerID`, `t`.`Address`, `t`.`City`, `t`.`CompanyName`, `t`.`ContactName`, `t`.`ContactTitle`, `t`.`Country`, `t`.`Fax`, `t`.`Phone`, `t`.`PostalCode`, `t`.`Region`, `o`.`OrderID`, `o`.`CustomerID`, `o`.`EmployeeID`, `o`.`OrderDate` -FROM ( - SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region`, `c`.`CustomerID` = 'ALFKI' AS `c` - FROM `Customers` AS `c` - WHERE `c`.`CustomerID` LIKE 'A%' - ORDER BY `c`.`CustomerID` = 'ALFKI' - LIMIT 18446744073709551610 OFFSET @__p_1 -) AS `t` -LEFT JOIN `Orders` AS `o` ON `t`.`CustomerID` = `o`.`CustomerID` -ORDER BY `t`.`c`, `t`.`CustomerID` -"""); + if (SingleStoreTestHelpers.HasPrimitiveCollectionsSupport(Fixture)) + { + AssertSql( + """ + @__p_1='1' + SELECT `t`.`CustomerID`, `t`.`Address`, `t`.`City`, `t`.`CompanyName`, `t`.`ContactName`, `t`.`ContactTitle`, `t`.`Country`, `t`.`Fax`, `t`.`Phone`, `t`.`PostalCode`, `t`.`Region`, `o`.`OrderID`, `o`.`CustomerID`, `o`.`EmployeeID`, `o`.`OrderDate` + FROM ( + SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region`, COALESCE(`c`.`CustomerID` IN ( + SELECT `l`.`value` + FROM JSON_TABLE('["ALFKI"]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` char(5) PATH '$[0]' + )) AS `l` + ), FALSE) AS `c` + FROM `Customers` AS `c` + WHERE `c`.`CustomerID` LIKE 'A%' + ORDER BY COALESCE(`c`.`CustomerID` IN ( + SELECT `l`.`value` + FROM JSON_TABLE('["ALFKI"]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` char(5) PATH '$[0]' + )) AS `l` + ), FALSE) + LIMIT 18446744073709551610 OFFSET @__p_1 + ) AS `t` + LEFT JOIN `Orders` AS `o` ON `t`.`CustomerID` = `o`.`CustomerID` + ORDER BY `t`.`c`, `t`.`CustomerID` + """); + } + else + { + AssertSql( + """ + @__p_1='1' + + SELECT `t`.`CustomerID`, `t`.`Address`, `t`.`City`, `t`.`CompanyName`, `t`.`ContactName`, `t`.`ContactTitle`, `t`.`Country`, `t`.`Fax`, `t`.`Phone`, `t`.`PostalCode`, `t`.`Region`, `o`.`OrderID`, `o`.`CustomerID`, `o`.`EmployeeID`, `o`.`OrderDate` + FROM ( + SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region`, `c`.`CustomerID` = 'ALFKI' AS `c` + FROM `Customers` AS `c` + WHERE `c`.`CustomerID` LIKE 'A%' + ORDER BY `c`.`CustomerID` = 'ALFKI' + LIMIT 18446744073709551610 OFFSET @__p_1 + ) AS `t` + LEFT JOIN `Orders` AS `o` ON `t`.`CustomerID` = `o`.`CustomerID` + ORDER BY `t`.`c`, `t`.`CustomerID` + """); + } } public override async Task Multi_level_includes_are_applied_with_skip(bool async) @@ -1526,8 +1607,7 @@ from c2 in ss.Set().Include(c => c.Orders).OrderBy(c => c.CustomerID). { AssertInclude(e.c1, a.c1, new ExpectedInclude(c => c.Orders)); AssertInclude(e.c2, a.c2, new ExpectedInclude(c => c.Orders)); - }, - entryCount: 15); + }); AssertSql( """ @@ -1571,7 +1651,7 @@ public override async Task Include_reference(bool async) SELECT `o`.`OrderID`, `o`.`CustomerID`, `o`.`EmployeeID`, `o`.`OrderDate`, `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Orders` AS `o` LEFT JOIN `Customers` AS `c` ON `o`.`CustomerID` = `c`.`CustomerID` -WHERE `o`.`CustomerID` IS NOT NULL AND (`o`.`CustomerID` LIKE 'F%') +WHERE `o`.`CustomerID` LIKE 'F%' """); } @@ -1801,7 +1881,7 @@ public override async Task Include_collection_and_reference(bool async) FROM `Orders` AS `o` LEFT JOIN `Customers` AS `c` ON `o`.`CustomerID` = `c`.`CustomerID` LEFT JOIN `Order Details` AS `o0` ON `o`.`OrderID` = `o0`.`OrderID` -WHERE `o`.`CustomerID` IS NOT NULL AND (`o`.`CustomerID` LIKE 'F%') +WHERE `o`.`CustomerID` LIKE 'F%' ORDER BY `o`.`OrderID`, `c`.`CustomerID`, `o0`.`OrderID` """); } @@ -1889,21 +1969,53 @@ public override async Task Include_collection_OrderBy_empty_list_does_not_contai { await base.Include_collection_OrderBy_empty_list_does_not_contains(async); - AssertSql( -""" -@__p_1='1' - -SELECT `t`.`CustomerID`, `t`.`Address`, `t`.`City`, `t`.`CompanyName`, `t`.`ContactName`, `t`.`ContactTitle`, `t`.`Country`, `t`.`Fax`, `t`.`Phone`, `t`.`PostalCode`, `t`.`Region`, `o`.`OrderID`, `o`.`CustomerID`, `o`.`EmployeeID`, `o`.`OrderDate` -FROM ( - SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region`, TRUE AS `c` - FROM `Customers` AS `c` - WHERE `c`.`CustomerID` LIKE 'A%' - ORDER BY (SELECT 1) - LIMIT 18446744073709551610 OFFSET @__p_1 -) AS `t` -LEFT JOIN `Orders` AS `o` ON `t`.`CustomerID` = `o`.`CustomerID` -ORDER BY `t`.`c`, `t`.`CustomerID` -"""); + if (SingleStoreTestHelpers.HasPrimitiveCollectionsSupport(Fixture)) + { + AssertSql( + """ + @__p_1='1' + SELECT `t`.`CustomerID`, `t`.`Address`, `t`.`City`, `t`.`CompanyName`, `t`.`ContactName`, `t`.`ContactTitle`, `t`.`Country`, `t`.`Fax`, `t`.`Phone`, `t`.`PostalCode`, `t`.`Region`, `o`.`OrderID`, `o`.`CustomerID`, `o`.`EmployeeID`, `o`.`OrderDate` + FROM ( + SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region`, NOT (COALESCE(`c`.`CustomerID` IN ( + SELECT `l`.`value` + FROM JSON_TABLE('[]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` char(5) PATH '$[0]' + )) AS `l` + ), FALSE)) AS `c` + FROM `Customers` AS `c` + WHERE `c`.`CustomerID` LIKE 'A%' + ORDER BY NOT (COALESCE(`c`.`CustomerID` IN ( + SELECT `l`.`value` + FROM JSON_TABLE('[]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` char(5) PATH '$[0]' + )) AS `l` + ), FALSE)) + LIMIT 18446744073709551610 OFFSET @__p_1 + ) AS `t` + LEFT JOIN `Orders` AS `o` ON `t`.`CustomerID` = `o`.`CustomerID` + ORDER BY `t`.`c`, `t`.`CustomerID` + """); + } + else + { + AssertSql( + """ + @__p_1='1' + + SELECT `t`.`CustomerID`, `t`.`Address`, `t`.`City`, `t`.`CompanyName`, `t`.`ContactName`, `t`.`ContactTitle`, `t`.`Country`, `t`.`Fax`, `t`.`Phone`, `t`.`PostalCode`, `t`.`Region`, `o`.`OrderID`, `o`.`CustomerID`, `o`.`EmployeeID`, `o`.`OrderDate` + FROM ( + SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region`, TRUE AS `c` + FROM `Customers` AS `c` + WHERE `c`.`CustomerID` LIKE 'A%' + ORDER BY (SELECT 1) + LIMIT 18446744073709551610 OFFSET @__p_1 + ) AS `t` + LEFT JOIN `Orders` AS `o` ON `t`.`CustomerID` = `o`.`CustomerID` + ORDER BY `t`.`c`, `t`.`CustomerID` + """); + } } public override async Task Include_multiple_references_then_include_multi_level_reverse(bool async) @@ -1931,7 +2043,7 @@ public override async Task Include_reference_and_collection(bool async) FROM `Orders` AS `o` LEFT JOIN `Customers` AS `c` ON `o`.`CustomerID` = `c`.`CustomerID` LEFT JOIN `Order Details` AS `o0` ON `o`.`OrderID` = `o0`.`OrderID` -WHERE `o`.`CustomerID` IS NOT NULL AND (`o`.`CustomerID` LIKE 'F%') +WHERE `o`.`CustomerID` LIKE 'F%' ORDER BY `o`.`OrderID`, `c`.`CustomerID`, `o0`.`OrderID` """); } @@ -2003,7 +2115,7 @@ public override async Task Include_reference_and_collection_order_by(bool async) FROM `Orders` AS `o` LEFT JOIN `Customers` AS `c` ON `o`.`CustomerID` = `c`.`CustomerID` LEFT JOIN `Orders` AS `o0` ON `c`.`CustomerID` = `o0`.`CustomerID` -WHERE `o`.`CustomerID` IS NOT NULL AND (`o`.`CustomerID` LIKE 'F%') +WHERE `o`.`CustomerID` LIKE 'F%' ORDER BY `o`.`OrderID`, `c`.`CustomerID` """); } @@ -2303,6 +2415,21 @@ public override async Task Include_collection_with_client_filter(bool async) AssertSql(); } + public override async Task Outer_identifier_correctly_determined_when_doing_include_on_right_side_of_left_join(bool async) + { + await base.Outer_identifier_correctly_determined_when_doing_include_on_right_side_of_left_join(async); + + AssertSql( + """ + SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region`, `o`.`OrderID`, `o`.`CustomerID`, `o`.`EmployeeID`, `o`.`OrderDate`, `o0`.`OrderID`, `o0`.`ProductID`, `o0`.`Discount`, `o0`.`Quantity`, `o0`.`UnitPrice` + FROM `Customers` AS `c` + LEFT JOIN `Orders` AS `o` ON `c`.`CustomerID` = `o`.`CustomerID` + LEFT JOIN `Order Details` AS `o0` ON `o`.`OrderID` = `o0`.`OrderID` + WHERE `c`.`City` = 'Seattle' + ORDER BY `c`.`CustomerID`, `o`.`OrderID`, `o0`.`OrderID` + """); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindFunctionsQuerySingleStoreTest.SingleStore.cs b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindFunctionsQuerySingleStoreTest.SingleStore.cs index 47423ff91..b74bd2f2e 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindFunctionsQuerySingleStoreTest.SingleStore.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindFunctionsQuerySingleStoreTest.SingleStore.cs @@ -14,9 +14,7 @@ public async Task PadLeft_without_second_arg(bool async) { await AssertQuery( async, - ss => ss.Set().Where(r => r.CustomerID.PadLeft(8) == " ALFKI"), - entryCount: 1); - + ss => ss.Set().Where(r => r.CustomerID.PadLeft(8) == " ALFKI")); AssertSql( @"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` @@ -29,8 +27,7 @@ public async Task PadLeft_with_second_arg(bool async) { await AssertQuery( async, - ss => ss.Set().Where(r => r.CustomerID.PadLeft(8, 'x') == "xxxALFKI"), - entryCount: 1); + ss => ss.Set().Where(r => r.CustomerID.PadLeft(8, 'x') == "xxxALFKI")); AssertSql( @"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` @@ -44,8 +41,7 @@ public async Task PadRight_without_second_arg(bool async) { await AssertQuery( async, - ss => ss.Set().Where(r => r.CustomerID.PadRight(8) == "ALFKI "), - entryCount: 1); + ss => ss.Set().Where(r => r.CustomerID.PadRight(8) == "ALFKI ")); AssertSql( @"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` @@ -59,8 +55,7 @@ public async Task PadRight_with_second_arg(bool async) { await AssertQuery( async, - ss => ss.Set().Where(r => r.CustomerID.PadRight(8, 'c') == "ALFKIccc"), - entryCount: 1); + ss => ss.Set().Where(r => r.CustomerID.PadRight(8, 'c') == "ALFKIccc")); AssertSql( @"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindFunctionsQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindFunctionsQuerySingleStoreTest.cs index 87adecf46..65616342a 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindFunctionsQuerySingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindFunctionsQuerySingleStoreTest.cs @@ -4,7 +4,6 @@ using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.TestUtilities; using EntityFrameworkCore.SingleStore.FunctionalTests.TestUtilities; -using EntityFrameworkCore.SingleStore.Tests; using Microsoft.EntityFrameworkCore.TestModels.Northwind; using Xunit; using Xunit.Abstractions; @@ -32,9 +31,11 @@ public override async Task String_StartsWith_Literal(bool async) await base.String_StartsWith_Literal(async); AssertSql( - $@"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`ContactName` IS NOT NULL AND (`c`.`ContactName` LIKE 'M%')"); +WHERE `c`.`ContactName` LIKE 'M%' +"""); } [ConditionalTheory] @@ -43,9 +44,11 @@ public override async Task String_StartsWith_Identity(bool async) await base.String_StartsWith_Identity(async); AssertSql( - $@"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE (`c`.`ContactName` = '') OR (`c`.`ContactName` IS NOT NULL AND (LEFT(`c`.`ContactName`, CHAR_LENGTH(`c`.`ContactName`)) = `c`.`ContactName`))"); +WHERE `c`.`ContactName` IS NOT NULL AND (LEFT(`c`.`ContactName`, CHAR_LENGTH(`c`.`ContactName`)) = `c`.`ContactName`) +"""); } [ConditionalTheory] @@ -54,9 +57,11 @@ public override async Task String_StartsWith_Column(bool async) await base.String_StartsWith_Column(async); AssertSql( - $@"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE (`c`.`ContactName` = '') OR (`c`.`ContactName` IS NOT NULL AND (LEFT(`c`.`ContactName`, CHAR_LENGTH(`c`.`ContactName`)) = `c`.`ContactName`))"); +WHERE `c`.`ContactName` IS NOT NULL AND (LEFT(`c`.`ContactName`, CHAR_LENGTH(`c`.`ContactName`)) = `c`.`ContactName`) +"""); } [ConditionalTheory] @@ -65,9 +70,11 @@ public override async Task String_StartsWith_MethodCall(bool async) await base.String_StartsWith_MethodCall(async); AssertSql( - $@"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`ContactName` IS NOT NULL AND (`c`.`ContactName` LIKE 'M%')"); +WHERE `c`.`ContactName` LIKE 'M%' +"""); } [ConditionalTheory] @@ -76,9 +83,11 @@ public override async Task String_EndsWith_Literal(bool async) await base.String_EndsWith_Literal(async); AssertSql( - $@"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`ContactName` IS NOT NULL AND (`c`.`ContactName` LIKE '%b')"); +WHERE `c`.`ContactName` LIKE '%b' +"""); } [ConditionalTheory] @@ -87,9 +96,11 @@ public override async Task String_EndsWith_Identity(bool async) await base.String_EndsWith_Identity(async); AssertSql( - $@"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE (`c`.`ContactName` = '') OR (`c`.`ContactName` IS NOT NULL AND (RIGHT(`c`.`ContactName`, CHAR_LENGTH(`c`.`ContactName`)) = `c`.`ContactName`))"); +WHERE `c`.`ContactName` IS NOT NULL AND (RIGHT(`c`.`ContactName`, CHAR_LENGTH(`c`.`ContactName`)) = `c`.`ContactName`) +"""); } [ConditionalTheory] @@ -98,9 +109,11 @@ public override async Task String_EndsWith_Column(bool async) await base.String_EndsWith_Column(async); AssertSql( - $@"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE (`c`.`ContactName` = '') OR (`c`.`ContactName` IS NOT NULL AND (RIGHT(`c`.`ContactName`, CHAR_LENGTH(`c`.`ContactName`)) = `c`.`ContactName`))"); +WHERE `c`.`ContactName` IS NOT NULL AND (RIGHT(`c`.`ContactName`, CHAR_LENGTH(`c`.`ContactName`)) = `c`.`ContactName`) +"""); } [ConditionalTheory] @@ -109,9 +122,11 @@ public override async Task String_EndsWith_MethodCall(bool async) await base.String_EndsWith_MethodCall(async); AssertSql( - $@"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`ContactName` IS NOT NULL AND (`c`.`ContactName` LIKE '%m')"); +WHERE `c`.`ContactName` LIKE '%m' +"""); } [ConditionalTheory(Skip = "SingleStore LIKE operator is case insensitive")] @@ -120,9 +135,11 @@ public override async Task String_Contains_Literal(bool async) await base.String_Contains_Literal(async); AssertSql( - $@"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`ContactName` LIKE '%M%'"); +WHERE `c`.`ContactName` LIKE '%M%' +"""); } [ConditionalTheory] @@ -131,9 +148,11 @@ public override async Task String_Contains_Identity(bool async) await base.String_Contains_Identity(async); AssertSql( - $@"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE (`c`.`ContactName` LIKE '') OR (LOCATE(`c`.`ContactName`, `c`.`ContactName`) > 0)"); +WHERE `c`.`ContactName` IS NOT NULL AND ((LOCATE(`c`.`ContactName`, `c`.`ContactName`) > 0) OR (`c`.`ContactName` LIKE '')) +"""); } [ConditionalTheory] @@ -142,9 +161,11 @@ public override async Task String_Contains_Column(bool async) await base.String_Contains_Column(async); AssertSql( - $@"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE (`c`.`ContactName` LIKE '') OR (LOCATE(`c`.`ContactName`, `c`.`ContactName`) > 0)"); +WHERE `c`.`ContactName` IS NOT NULL AND ((LOCATE(`c`.`ContactName`, `c`.`ContactName`) > 0) OR (`c`.`ContactName` LIKE '')) +"""); } [ConditionalTheory(Skip = "SingleStore LIKE operator is case insensitive")] @@ -333,9 +354,11 @@ public override async Task Where_math_abs_uncorrelated(bool async) await base.Where_math_abs_uncorrelated(async); AssertSql( - @"SELECT `o`.`OrderID`, `o`.`ProductID`, `o`.`Discount`, `o`.`Quantity`, `o`.`UnitPrice` +""" +SELECT `o`.`OrderID`, `o`.`ProductID`, `o`.`Discount`, `o`.`Quantity`, `o`.`UnitPrice` FROM `Order Details` AS `o` -WHERE (`o`.`UnitPrice` < 7.0) AND (10 < `o`.`ProductID`)"); +WHERE (`o`.`UnitPrice` < 7.0) AND (10 < `o`.`ProductID`) +"""); } [ConditionalTheory] @@ -511,11 +534,13 @@ public override async Task String_Contains_parameter_with_whitespace(bool async) await base.String_Contains_parameter_with_whitespace(async); AssertSql( - $@"@__pattern_0=' ' (Size = 4000) +""" +@__pattern_0_rewritten='% %' (Size = 30) SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE (@__pattern_0 LIKE '') OR (LOCATE(@__pattern_0, `c`.`ContactName`) > 0)"); +WHERE `c`.`ContactName` LIKE @__pattern_0_rewritten +"""); } public override async Task String_LastOrDefault_MethodCall(bool async) @@ -889,29 +914,41 @@ public override async Task String_Compare_simple_zero(bool async) await base.String_Compare_simple_zero(async); AssertSql( - $@"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` = 'ALFKI'", +WHERE `c`.`CustomerID` = 'AROUT' +""", // - $@"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` <> 'ALFKI'", +WHERE `c`.`CustomerID` <> 'AROUT' +""", // - $@"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` > 'ALFKI'", +WHERE `c`.`CustomerID` > 'AROUT' +""", // - $@"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` <= 'ALFKI'", +WHERE `c`.`CustomerID` <= 'AROUT' +""", // - $@"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` > 'ALFKI'", +WHERE `c`.`CustomerID` > 'AROUT' +""", // - $@"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` <= 'ALFKI'"); +WHERE `c`.`CustomerID` <= 'AROUT' +"""); } public override async Task String_Compare_simple_one(bool async) @@ -919,29 +956,41 @@ public override async Task String_Compare_simple_one(bool async) await base.String_Compare_simple_one(async); AssertSql( - $@"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` > 'ALFKI'", +WHERE `c`.`CustomerID` > 'AROUT' +""", // - $@"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` < 'ALFKI'", +WHERE `c`.`CustomerID` < 'AROUT' +""", // - $@"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` <= 'ALFKI'", +WHERE `c`.`CustomerID` <= 'AROUT' +""", // - $@"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` <= 'ALFKI'", +WHERE `c`.`CustomerID` <= 'AROUT' +""", // - $@"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` >= 'ALFKI'", +WHERE `c`.`CustomerID` >= 'AROUT' +""", // - $@"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` >= 'ALFKI'"); +WHERE `c`.`CustomerID` >= 'AROUT' +"""); } public override async Task String_compare_with_parameter(bool async) @@ -949,41 +998,53 @@ public override async Task String_compare_with_parameter(bool async) await base.String_compare_with_parameter(async); AssertSql( - $@"@__customer_CustomerID_0='ALFKI' (Size = {SingleStoreTestHelpers.Instance.GetIndexedStringPropertyDefaultLength}) +""" +@__customer_CustomerID_0='AROUT' (Size = 5) (DbType = StringFixedLength) SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` > @__customer_CustomerID_0", +WHERE `c`.`CustomerID` > @__customer_CustomerID_0 +""", // - $@"@__customer_CustomerID_0='ALFKI' (Size = {SingleStoreTestHelpers.Instance.GetIndexedStringPropertyDefaultLength}) +""" +@__customer_CustomerID_0='AROUT' (Size = 5) (DbType = StringFixedLength) SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` < @__customer_CustomerID_0", +WHERE `c`.`CustomerID` < @__customer_CustomerID_0 +""", // - $@"@__customer_CustomerID_0='ALFKI' (Size = {SingleStoreTestHelpers.Instance.GetIndexedStringPropertyDefaultLength}) +""" +@__customer_CustomerID_0='AROUT' (Size = 5) (DbType = StringFixedLength) SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` <= @__customer_CustomerID_0", +WHERE `c`.`CustomerID` <= @__customer_CustomerID_0 +""", // - $@"@__customer_CustomerID_0='ALFKI' (Size = {SingleStoreTestHelpers.Instance.GetIndexedStringPropertyDefaultLength}) +""" +@__customer_CustomerID_0='AROUT' (Size = 5) (DbType = StringFixedLength) SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` <= @__customer_CustomerID_0", +WHERE `c`.`CustomerID` <= @__customer_CustomerID_0 +""", // - $@"@__customer_CustomerID_0='ALFKI' (Size = {SingleStoreTestHelpers.Instance.GetIndexedStringPropertyDefaultLength}) +""" +@__customer_CustomerID_0='AROUT' (Size = 5) (DbType = StringFixedLength) SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` >= @__customer_CustomerID_0", +WHERE `c`.`CustomerID` >= @__customer_CustomerID_0 +""", // - $@"@__customer_CustomerID_0='ALFKI' (Size = {SingleStoreTestHelpers.Instance.GetIndexedStringPropertyDefaultLength}) +""" +@__customer_CustomerID_0='AROUT' (Size = 5) (DbType = StringFixedLength) SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` >= @__customer_CustomerID_0"); +WHERE `c`.`CustomerID` >= @__customer_CustomerID_0 +"""); } public override async Task String_Compare_simple_more_than_one(bool async) @@ -1065,29 +1126,41 @@ public override async Task String_Compare_to_simple_zero(bool async) await base.String_Compare_to_simple_zero(async); AssertSql( - $@"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` = 'ALFKI'", +WHERE `c`.`CustomerID` = 'AROUT' +""", // - $@"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` <> 'ALFKI'", +WHERE `c`.`CustomerID` <> 'AROUT' +""", // - $@"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` > 'ALFKI'", +WHERE `c`.`CustomerID` > 'AROUT' +""", // - $@"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` <= 'ALFKI'", +WHERE `c`.`CustomerID` <= 'AROUT' +""", // - $@"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` > 'ALFKI'", +WHERE `c`.`CustomerID` > 'AROUT' +""", // - $@"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` <= 'ALFKI'"); +WHERE `c`.`CustomerID` <= 'AROUT' +"""); } public override async Task String_Compare_to_simple_one(bool async) @@ -1095,29 +1168,41 @@ public override async Task String_Compare_to_simple_one(bool async) await base.String_Compare_to_simple_one(async); AssertSql( - $@"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` > 'ALFKI'", +WHERE `c`.`CustomerID` > 'AROUT' +""", // - $@"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` < 'ALFKI'", +WHERE `c`.`CustomerID` < 'AROUT' +""", // - $@"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` <= 'ALFKI'", +WHERE `c`.`CustomerID` <= 'AROUT' +""", // - $@"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` <= 'ALFKI'", +WHERE `c`.`CustomerID` <= 'AROUT' +""", // - $@"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` >= 'ALFKI'", +WHERE `c`.`CustomerID` >= 'AROUT' +""", // - $@"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` >= 'ALFKI'"); +WHERE `c`.`CustomerID` >= 'AROUT' +"""); } public override async Task String_compare_to_with_parameter(bool async) @@ -1125,41 +1210,53 @@ public override async Task String_compare_to_with_parameter(bool async) await base.String_compare_to_with_parameter(async); AssertSql( - $@"@__customer_CustomerID_0='ALFKI' (Size = {SingleStoreTestHelpers.Instance.GetIndexedStringPropertyDefaultLength}) +""" +@__customer_CustomerID_0='AROUT' (Size = 5) (DbType = StringFixedLength) SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` > @__customer_CustomerID_0", +WHERE `c`.`CustomerID` > @__customer_CustomerID_0 +""", // - $@"@__customer_CustomerID_0='ALFKI' (Size = {SingleStoreTestHelpers.Instance.GetIndexedStringPropertyDefaultLength}) +""" +@__customer_CustomerID_0='AROUT' (Size = 5) (DbType = StringFixedLength) SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` < @__customer_CustomerID_0", +WHERE `c`.`CustomerID` < @__customer_CustomerID_0 +""", // - $@"@__customer_CustomerID_0='ALFKI' (Size = {SingleStoreTestHelpers.Instance.GetIndexedStringPropertyDefaultLength}) +""" +@__customer_CustomerID_0='AROUT' (Size = 5) (DbType = StringFixedLength) SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` <= @__customer_CustomerID_0", +WHERE `c`.`CustomerID` <= @__customer_CustomerID_0 +""", // - $@"@__customer_CustomerID_0='ALFKI' (Size = {SingleStoreTestHelpers.Instance.GetIndexedStringPropertyDefaultLength}) +""" +@__customer_CustomerID_0='AROUT' (Size = 5) (DbType = StringFixedLength) SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` <= @__customer_CustomerID_0", +WHERE `c`.`CustomerID` <= @__customer_CustomerID_0 +""", // - $@"@__customer_CustomerID_0='ALFKI' (Size = {SingleStoreTestHelpers.Instance.GetIndexedStringPropertyDefaultLength}) +""" +@__customer_CustomerID_0='AROUT' (Size = 5) (DbType = StringFixedLength) SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` >= @__customer_CustomerID_0", +WHERE `c`.`CustomerID` >= @__customer_CustomerID_0 +""", // - $@"@__customer_CustomerID_0='ALFKI' (Size = {SingleStoreTestHelpers.Instance.GetIndexedStringPropertyDefaultLength}) +""" +@__customer_CustomerID_0='AROUT' (Size = 5) (DbType = StringFixedLength) SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` >= @__customer_CustomerID_0"); +WHERE `c`.`CustomerID` >= @__customer_CustomerID_0 +"""); } public override async Task String_Compare_to_simple_more_than_one(bool async) @@ -1197,29 +1294,41 @@ public override async Task String_Compare_to_nested(bool async) await base.String_Compare_to_nested(async); AssertSql( - $@"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` = (CONCAT('M', `c`.`CustomerID`))", +WHERE `c`.`CustomerID` <> (CONCAT('M', `c`.`CustomerID`)) +""", // - $@"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` <> UPPER(`c`.`CustomerID`)", +WHERE `c`.`CustomerID` = UPPER(`c`.`CustomerID`) +""", // - $@"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` > REPLACE('ALFKI', 'ALF', `c`.`CustomerID`)", +WHERE `c`.`CustomerID` > REPLACE('AROUT', 'OUT', `c`.`CustomerID`) +""", // - $@"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` <= (CONCAT('M', `c`.`CustomerID`))", +WHERE `c`.`CustomerID` <= (CONCAT('M', `c`.`CustomerID`)) +""", // - $@"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` > UPPER(`c`.`CustomerID`)", +WHERE `c`.`CustomerID` > UPPER(`c`.`CustomerID`) +""", // - $@"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -WHERE `c`.`CustomerID` < REPLACE('ALFKI', 'ALF', `c`.`CustomerID`)"); +WHERE `c`.`CustomerID` < REPLACE('AROUT', 'OUT', `c`.`CustomerID`) +"""); } public override async Task String_Compare_to_multi_predicate(bool async) @@ -1696,6 +1805,607 @@ public override async Task Convert_ToString(bool async) $@"SELECT `o`.`OrderID`, `o`.`CustomerID`, `o`.`EmployeeID`, `o`.`OrderDate` FROM `Orders` AS `o` WHERE (`o`.`CustomerID` = 'ALFKI') AND ((CAST(`o`.`OrderDate` AS char) LIKE '%1997%') OR (CAST(`o`.`OrderDate` AS char) LIKE '%1998%'))"); + } + + public override async Task String_StartsWith_Parameter(bool async) + { + await base.String_StartsWith_Parameter(async); + + AssertSql( +""" +@__pattern_0_rewritten='M%' (Size = 30) + +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +FROM `Customers` AS `c` +WHERE `c`.`ContactName` LIKE @__pattern_0_rewritten +"""); + } + + public override async Task String_EndsWith_Parameter(bool async) + { + await base.String_EndsWith_Parameter(async); + + AssertSql( +""" +@__pattern_0_rewritten='%b' (Size = 30) + +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +FROM `Customers` AS `c` +WHERE `c`.`ContactName` LIKE @__pattern_0_rewritten +"""); + } + + public override async Task String_Join_over_non_nullable_column(bool async) + { + await base.String_Join_over_non_nullable_column(async); + + AssertSql( +""" +SELECT `t`.`City`, `c0`.`CustomerID` +FROM ( + SELECT `c`.`City` + FROM `Customers` AS `c` + GROUP BY `c`.`City` +) AS `t` +LEFT JOIN `Customers` AS `c0` ON `t`.`City` = `c0`.`City` +ORDER BY `t`.`City` +"""); + } + + public override async Task String_Join_with_predicate(bool async) + { + await base.String_Join_with_predicate(async); + + AssertSql( +""" +SELECT `t`.`City`, `t0`.`CustomerID` +FROM ( + SELECT `c`.`City` + FROM `Customers` AS `c` + GROUP BY `c`.`City` +) AS `t` +LEFT JOIN ( + SELECT `c0`.`CustomerID`, `c0`.`City` + FROM `Customers` AS `c0` + WHERE CHAR_LENGTH(`c0`.`ContactName`) > 10 +) AS `t0` ON `t`.`City` = `t0`.`City` +ORDER BY `t`.`City` +"""); + } + + public override async Task String_Join_with_ordering(bool async) + { + await base.String_Join_with_ordering(async); + + AssertSql( +""" +SELECT `t`.`City`, `c0`.`CustomerID` +FROM ( + SELECT `c`.`City` + FROM `Customers` AS `c` + GROUP BY `c`.`City` +) AS `t` +LEFT JOIN `Customers` AS `c0` ON `t`.`City` = `c0`.`City` +ORDER BY `t`.`City`, `c0`.`CustomerID` DESC +"""); + } + + public override async Task String_Join_over_nullable_column(bool async) + { + await base.String_Join_over_nullable_column(async); + + AssertSql( +""" +SELECT `t`.`City`, `c0`.`Region`, `c0`.`CustomerID` +FROM ( + SELECT `c`.`City` + FROM `Customers` AS `c` + GROUP BY `c`.`City` +) AS `t` +LEFT JOIN `Customers` AS `c0` ON `t`.`City` = `c0`.`City` +ORDER BY `t`.`City` +"""); + } + + public override async Task String_Concat(bool async) + { + await base.String_Concat(async); + + AssertSql( +""" +SELECT `t`.`City`, `c0`.`CustomerID` +FROM ( + SELECT `c`.`City` + FROM `Customers` AS `c` + GROUP BY `c`.`City` +) AS `t` +LEFT JOIN `Customers` AS `c0` ON `t`.`City` = `c0`.`City` +ORDER BY `t`.`City` +"""); + } + + public override async Task Where_math_square(bool async) + { + await base.Where_math_square(async); + + AssertSql( +$""" +SELECT `o`.`OrderID`, `o`.`ProductID`, `o`.`Discount`, `o`.`Quantity`, `o`.`UnitPrice` +FROM `Order Details` AS `o` +WHERE POWER({SingleStoreTestHelpers.CastAsDouble("`o`.`Discount`")}, 2.0) > 0.05000000074505806 +"""); + } + + public override async Task Sum_over_round_works_correctly_in_projection(bool async) + { + await base.Sum_over_round_works_correctly_in_projection(async); + + AssertSql( +""" +SELECT `o`.`OrderID`, ( + SELECT COALESCE(SUM(ROUND(`o0`.`UnitPrice`, 2)), 0.0) + FROM `Order Details` AS `o0` + WHERE `o`.`OrderID` = `o0`.`OrderID`) AS `Sum` +FROM `Orders` AS `o` +WHERE `o`.`OrderID` < 10300 +"""); + } + + public override async Task Sum_over_round_works_correctly_in_projection_2(bool async) + { + await base.Sum_over_round_works_correctly_in_projection_2(async); + + AssertSql( +""" +SELECT `o`.`OrderID`, ( + SELECT COALESCE(SUM(ROUND(`o0`.`UnitPrice` * `o0`.`UnitPrice`, 2)), 0.0) + FROM `Order Details` AS `o0` + WHERE `o`.`OrderID` = `o0`.`OrderID`) AS `Sum` +FROM `Orders` AS `o` +WHERE `o`.`OrderID` < 10300 +"""); + } + + public override async Task Sum_over_truncate_works_correctly_in_projection(bool async) + { + await base.Sum_over_truncate_works_correctly_in_projection(async); + + AssertSql( +""" +SELECT `o`.`OrderID`, ( + SELECT COALESCE(SUM(TRUNCATE(`o0`.`UnitPrice`, 0)), 0.0) + FROM `Order Details` AS `o0` + WHERE `o`.`OrderID` = `o0`.`OrderID`) AS `Sum` +FROM `Orders` AS `o` +WHERE `o`.`OrderID` < 10300 +"""); + } + + public override async Task Sum_over_truncate_works_correctly_in_projection_2(bool async) + { + await base.Sum_over_truncate_works_correctly_in_projection_2(async); + + AssertSql( +""" +SELECT `o`.`OrderID`, ( + SELECT COALESCE(SUM(TRUNCATE(`o0`.`UnitPrice` * `o0`.`UnitPrice`, 0)), 0.0) + FROM `Order Details` AS `o0` + WHERE `o`.`OrderID` = `o0`.`OrderID`) AS `Sum` +FROM `Orders` AS `o` +WHERE `o`.`OrderID` < 10300 +"""); + } + + public override async Task Where_math_degrees(bool async) + { + await base.Where_math_degrees(async); + + AssertSql( +$""" +SELECT `o`.`OrderID`, `o`.`ProductID`, `o`.`Discount`, `o`.`Quantity`, `o`.`UnitPrice` +FROM `Order Details` AS `o` +WHERE (`o`.`OrderID` = 11077) AND (DEGREES({SingleStoreTestHelpers.CastAsDouble("`o`.`Discount`")}) > 0.0) +"""); + } + + public override async Task Where_math_radians(bool async) + { + await base.Where_math_radians(async); + + AssertSql( +$""" +SELECT `o`.`OrderID`, `o`.`ProductID`, `o`.`Discount`, `o`.`Quantity`, `o`.`UnitPrice` +FROM `Order Details` AS `o` +WHERE (`o`.`OrderID` = 11077) AND (RADIANS({SingleStoreTestHelpers.CastAsDouble("`o`.`Discount`")}) > 0.0) +"""); + } + + public override async Task Where_mathf_abs1(bool async) + { + await base.Where_mathf_abs1(async); + + AssertSql( +$""" +SELECT `p`.`ProductID`, `p`.`Discontinued`, `p`.`ProductName`, `p`.`SupplierID`, `p`.`UnitPrice`, `p`.`UnitsInStock` +FROM `Products` AS `p` +WHERE ABS({SingleStoreTestHelpers.CastAsDouble("`p`.`ProductID`")}) > 10 +"""); + } + + public override async Task Where_mathf_ceiling1(bool async) + { + await base.Where_mathf_ceiling1(async); + + AssertSql( +""" +SELECT `o`.`OrderID`, `o`.`ProductID`, `o`.`Discount`, `o`.`Quantity`, `o`.`UnitPrice` +FROM `Order Details` AS `o` +WHERE (`o`.`UnitPrice` < 7.0) AND (CEILING(`o`.`Discount`) > 0) +"""); + } + + public override async Task Where_mathf_floor(bool async) + { + await base.Where_mathf_floor(async); + + AssertSql( +$""" +SELECT `o`.`OrderID`, `o`.`ProductID`, `o`.`Discount`, `o`.`Quantity`, `o`.`UnitPrice` +FROM `Order Details` AS `o` +WHERE (`o`.`Quantity` < 5) AND (FLOOR({SingleStoreTestHelpers.CastAsDouble("`o`.`UnitPrice`")}) > 10) +"""); + } + + public override async Task Where_mathf_power(bool async) + { + await base.Where_mathf_power(async); + + AssertSql( +""" +SELECT `o`.`OrderID`, `o`.`ProductID`, `o`.`Discount`, `o`.`Quantity`, `o`.`UnitPrice` +FROM `Order Details` AS `o` +WHERE POWER(`o`.`Discount`, 3) > 0.005 +"""); + } + + public override async Task Where_mathf_square(bool async) + { + await base.Where_mathf_square(async); + + AssertSql( +""" +SELECT `o`.`OrderID`, `o`.`ProductID`, `o`.`Discount`, `o`.`Quantity`, `o`.`UnitPrice` +FROM `Order Details` AS `o` +WHERE POWER(`o`.`Discount`, 2) > 0.05 +"""); + } + + public override async Task Where_mathf_round2(bool async) + { + await base.Where_mathf_round2(async); + + AssertSql( +$""" +SELECT `o`.`OrderID`, `o`.`ProductID`, `o`.`Discount`, `o`.`Quantity`, `o`.`UnitPrice` +FROM `Order Details` AS `o` +WHERE ROUND({SingleStoreTestHelpers.CastAsDouble("`o`.`UnitPrice`")}, 2) > 100 +"""); + } + + public override async Task Select_mathf_round(bool async) + { + await base.Select_mathf_round(async); + + AssertSql( +$""" +SELECT ROUND({SingleStoreTestHelpers.CastAsDouble("`o`.`OrderID`")}) +FROM `Orders` AS `o` +WHERE `o`.`OrderID` < 10250 +"""); + } + + public override async Task Select_mathf_round2(bool async) + { + await base.Select_mathf_round2(async); + + AssertSql( +$""" +SELECT ROUND({SingleStoreTestHelpers.CastAsDouble("`o`.`UnitPrice`")}, 2) +FROM `Order Details` AS `o` +WHERE `o`.`Quantity` < 5 +"""); + } + + public override async Task Where_mathf_truncate(bool async) + { + await base.Where_mathf_truncate(async); + + AssertSql( +$""" +SELECT `o`.`OrderID`, `o`.`ProductID`, `o`.`Discount`, `o`.`Quantity`, `o`.`UnitPrice` +FROM `Order Details` AS `o` +WHERE (`o`.`Quantity` < 5) AND (TRUNCATE({SingleStoreTestHelpers.CastAsDouble("`o`.`UnitPrice`")}, 0) > 10) +"""); + } + + public override async Task Select_mathf_truncate(bool async) + { + await base.Select_mathf_truncate(async); + + AssertSql( +$""" +SELECT TRUNCATE({SingleStoreTestHelpers.CastAsDouble("`o`.`UnitPrice`")}, 0) +FROM `Order Details` AS `o` +WHERE `o`.`Quantity` < 5 +"""); + } + + public override async Task Where_mathf_exp(bool async) + { + await base.Where_mathf_exp(async); + + AssertSql( +""" +SELECT `o`.`OrderID`, `o`.`ProductID`, `o`.`Discount`, `o`.`Quantity`, `o`.`UnitPrice` +FROM `Order Details` AS `o` +WHERE (`o`.`OrderID` = 11077) AND (EXP(`o`.`Discount`) > 1) +"""); + } + + public override async Task Where_mathf_log10(bool async) + { + await base.Where_mathf_log10(async); + + AssertSql( +""" +SELECT `o`.`OrderID`, `o`.`ProductID`, `o`.`Discount`, `o`.`Quantity`, `o`.`UnitPrice` +FROM `Order Details` AS `o` +WHERE ((`o`.`OrderID` = 11077) AND (`o`.`Discount` > 0)) AND (LOG10(`o`.`Discount`) < 0) +"""); + } + + public override async Task Where_mathf_log(bool async) + { + await base.Where_mathf_log(async); + + AssertSql( +""" +SELECT `o`.`OrderID`, `o`.`ProductID`, `o`.`Discount`, `o`.`Quantity`, `o`.`UnitPrice` +FROM `Order Details` AS `o` +WHERE ((`o`.`OrderID` = 11077) AND (`o`.`Discount` > 0)) AND (LOG(`o`.`Discount`) < 0) +"""); + } + + public override async Task Where_mathf_log_new_base(bool async) + { + await base.Where_mathf_log_new_base(async); + + AssertSql( +""" +SELECT `o`.`OrderID`, `o`.`ProductID`, `o`.`Discount`, `o`.`Quantity`, `o`.`UnitPrice` +FROM `Order Details` AS `o` +WHERE ((`o`.`OrderID` = 11077) AND (`o`.`Discount` > 0)) AND (LOG(`o`.`Discount`, 7) < 0) +"""); + } + + public override async Task Where_mathf_sqrt(bool async) + { + await base.Where_mathf_sqrt(async); + + AssertSql( +""" +SELECT `o`.`OrderID`, `o`.`ProductID`, `o`.`Discount`, `o`.`Quantity`, `o`.`UnitPrice` +FROM `Order Details` AS `o` +WHERE (`o`.`OrderID` = 11077) AND (SQRT(`o`.`Discount`) > 0) +"""); + } + + public override async Task Where_mathf_acos(bool async) + { + await base.Where_mathf_acos(async); + + AssertSql( +""" +SELECT `o`.`OrderID`, `o`.`ProductID`, `o`.`Discount`, `o`.`Quantity`, `o`.`UnitPrice` +FROM `Order Details` AS `o` +WHERE (`o`.`OrderID` = 11077) AND (ACOS(`o`.`Discount`) > 1) +"""); + } + + public override async Task Where_mathf_asin(bool async) + { + await base.Where_mathf_asin(async); + + AssertSql( +""" +SELECT `o`.`OrderID`, `o`.`ProductID`, `o`.`Discount`, `o`.`Quantity`, `o`.`UnitPrice` +FROM `Order Details` AS `o` +WHERE (`o`.`OrderID` = 11077) AND (ASIN(`o`.`Discount`) > 0) +"""); + } + + public override async Task Where_mathf_atan(bool async) + { + await base.Where_mathf_atan(async); + + AssertSql( +""" +SELECT `o`.`OrderID`, `o`.`ProductID`, `o`.`Discount`, `o`.`Quantity`, `o`.`UnitPrice` +FROM `Order Details` AS `o` +WHERE (`o`.`OrderID` = 11077) AND (ATAN(`o`.`Discount`) > 0) +"""); + } + + public override async Task Where_mathf_atan2(bool async) + { + await base.Where_mathf_atan2(async); + + AssertSql( +""" +SELECT `o`.`OrderID`, `o`.`ProductID`, `o`.`Discount`, `o`.`Quantity`, `o`.`UnitPrice` +FROM `Order Details` AS `o` +WHERE (`o`.`OrderID` = 11077) AND (ATAN2(`o`.`Discount`, 1) > 0) +"""); + } + + public override async Task Where_mathf_cos(bool async) + { + await base.Where_mathf_cos(async); + + AssertSql( +""" +SELECT `o`.`OrderID`, `o`.`ProductID`, `o`.`Discount`, `o`.`Quantity`, `o`.`UnitPrice` +FROM `Order Details` AS `o` +WHERE (`o`.`OrderID` = 11077) AND (COS(`o`.`Discount`) > 0) +"""); + } + + public override async Task Where_mathf_sin(bool async) + { + await base.Where_mathf_sin(async); + + AssertSql( +""" +SELECT `o`.`OrderID`, `o`.`ProductID`, `o`.`Discount`, `o`.`Quantity`, `o`.`UnitPrice` +FROM `Order Details` AS `o` +WHERE (`o`.`OrderID` = 11077) AND (SIN(`o`.`Discount`) > 0) +"""); + } + + public override async Task Where_mathf_tan(bool async) + { + await base.Where_mathf_tan(async); + + AssertSql( +""" +SELECT `o`.`OrderID`, `o`.`ProductID`, `o`.`Discount`, `o`.`Quantity`, `o`.`UnitPrice` +FROM `Order Details` AS `o` +WHERE (`o`.`OrderID` = 11077) AND (TAN(`o`.`Discount`) > 0) +"""); + } + + public override async Task Where_mathf_sign(bool async) + { + await base.Where_mathf_sign(async); + + AssertSql( +""" +SELECT `o`.`OrderID`, `o`.`ProductID`, `o`.`Discount`, `o`.`Quantity`, `o`.`UnitPrice` +FROM `Order Details` AS `o` +WHERE (`o`.`OrderID` = 11077) AND (SIGN(`o`.`Discount`) > 0) +"""); + } + + public override async Task Where_mathf_degrees(bool async) + { + await base.Where_mathf_degrees(async); + + AssertSql( +""" +SELECT `o`.`OrderID`, `o`.`ProductID`, `o`.`Discount`, `o`.`Quantity`, `o`.`UnitPrice` +FROM `Order Details` AS `o` +WHERE (`o`.`OrderID` = 11077) AND (DEGREES(`o`.`Discount`) > 0) +"""); + } + + public override async Task Where_mathf_radians(bool async) + { + await base.Where_mathf_radians(async); + + AssertSql( +""" +SELECT `o`.`OrderID`, `o`.`ProductID`, `o`.`Discount`, `o`.`Quantity`, `o`.`UnitPrice` +FROM `Order Details` AS `o` +WHERE (`o`.`OrderID` = 11077) AND (RADIANS(`o`.`Discount`) > 0) +"""); + } + + public override async Task Indexof_with_one_constant_arg(bool async) + { + await base.Indexof_with_one_constant_arg(async); + + AssertSql( +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +FROM `Customers` AS `c` +WHERE (LOCATE('a', `c`.`ContactName`) - 1) = 1 +"""); + } + + public override async Task Indexof_with_one_parameter_arg(bool async) + { + await base.Indexof_with_one_parameter_arg(async); + + AssertSql( +""" +@__pattern_0='a' (Size = 4000) + +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +FROM `Customers` AS `c` +WHERE (LOCATE(@__pattern_0, `c`.`ContactName`) - 1) = 1 +"""); + } + + public override async Task Indexof_with_constant_starting_position(bool async) + { + await base.Indexof_with_constant_starting_position(async); + + AssertSql( +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +FROM `Customers` AS `c` +WHERE (LOCATE('a', `c`.`ContactName`, 3) - 1) = 4 +"""); + } + + public override async Task Indexof_with_parameter_starting_position(bool async) + { + await base.Indexof_with_parameter_starting_position(async); + + AssertSql( +""" +@__start_0='2' + +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +FROM `Customers` AS `c` +WHERE (LOCATE('a', `c`.`ContactName`, @__start_0 + 1) - 1) = 4 +"""); + } + + public override async Task Replace_using_property_arguments(bool async) + { + await base.Replace_using_property_arguments(async); + + AssertSql( +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +FROM `Customers` AS `c` +WHERE REPLACE(`c`.`ContactName`, `c`.`ContactName`, `c`.`CustomerID`) = `c`.`CustomerID` +"""); + } + + public override async Task IsNullOrEmpty_negated_in_predicate(bool async) + { + await base.IsNullOrEmpty_negated_in_predicate(async); + + AssertSql( +""" +SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` +FROM `Customers` AS `c` +WHERE `c`.`Region` IS NOT NULL AND (`c`.`Region` <> '') +"""); + } + + public override async Task Where_DateOnly_FromDateTime(bool async) + { + await base.Where_DateOnly_FromDateTime(async); + + AssertSql( +""" +SELECT `o`.`OrderID`, `o`.`CustomerID`, `o`.`EmployeeID`, `o`.`OrderDate` +FROM `Orders` AS `o` +WHERE `o`.`OrderDate` IS NOT NULL AND (DATE(`o`.`OrderDate`) = '1996-09-16') +"""); } public override Task Datetime_subtraction_TotalDays(bool async) diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindGroupByQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindGroupByQuerySingleStoreTest.cs index 5fddce1b8..ab910d96b 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindGroupByQuerySingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindGroupByQuerySingleStoreTest.cs @@ -186,14 +186,14 @@ public override Task GroupBy_Count_in_projection(bool async) [SupportedServerVersionCondition("8.0.22-mysql", "0.0.0-mariadb")] public override Task GroupBy_group_Where_Select_Distinct_aggregate(bool async) { - // See https://github.com/mysql-net/SingleStoreConnector/issues/898. + // See https://github.com/mysql-net/MySqlConnector/issues/898. return base.GroupBy_group_Where_Select_Distinct_aggregate(async); } [SupportedServerVersionCondition("8.0.0-mysql", "0.0.0-mariadb")] // Is an issue issue in MySQL 5.7.34, but not in 8.0.25. public override Task GroupBy_constant_with_where_on_grouping_with_aggregate_operators(bool async) { - // See https://github.com/mysql-net/SingleStoreConnector/issues/980. + // See https://github.com/mysql-net/MySqlConnector/issues/980. return base.GroupBy_constant_with_where_on_grouping_with_aggregate_operators(async); } diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindIncludeNoTrackingQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindIncludeNoTrackingQuerySingleStoreTest.cs index 194f43646..b867c219a 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindIncludeNoTrackingQuerySingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindIncludeNoTrackingQuerySingleStoreTest.cs @@ -41,8 +41,7 @@ from c2 in ss.Set().OrderBy(c => c.CustomerID).Skip(2).Take(2) { AssertInclude(e.c1, a.c1, new ExpectedInclude(c => c.Orders)); AssertEqual(e.c2, a.c2); - }, - entryCount: 8); + }); } [ConditionalTheory(Skip = "SingleStore does not support this type of query: correlated subselect in ORDER BY")] @@ -96,8 +95,7 @@ public override Task Repro9735(bool async) .OrderBy(b => b.Customer.CustomerID != null) .ThenBy(b => b.Customer != null ? b.Customer.CustomerID : string.Empty) .ThenBy(b => b.EmployeeID) // Needs to be explicitly ordered by EmployeeID as well - .Take(2), - entryCount: 6); + .Take(2)); } [ConditionalTheory(Skip = "SingleStore does not support this type of query: correlated subselect in ORDER BY")] @@ -116,8 +114,7 @@ from c2 in ss.Set().Include(c => c.Orders).OrderBy(c => c.CustomerID). { AssertInclude(e.c1, a.c1, new ExpectedInclude(c => c.Orders)); AssertInclude(e.c2, a.c2, new ExpectedInclude(c => c.Orders)); - }, - entryCount: 15); + }); } [ConditionalTheory(Skip = "Feature 'scalar subselect inside the GROUP/ORDER BY of a pushed down query' is not supported by SingleStore")] @@ -142,8 +139,7 @@ public override Task Include_collection_with_multiple_conditional_order_by(bool .ThenBy(o => o.Customer != null ? o.Customer.City : string.Empty) .ThenBy(b => b.EmployeeID) // Needs to be explicitly ordered by EmployeeID as well .Take(5), - elementAsserter: (e, a) => AssertInclude(e, a, new ExpectedInclude(o => o.OrderDetails)), - entryCount: 14); + elementAsserter: (e, a) => AssertInclude(e, a, new ExpectedInclude(o => o.OrderDetails))); } private void AssertSql(params string[] expected) diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindIncludeQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindIncludeQuerySingleStoreTest.cs index 19f1ffab3..931a23422 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindIncludeQuerySingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindIncludeQuerySingleStoreTest.cs @@ -43,8 +43,7 @@ public override Task Include_collection_with_multiple_conditional_order_by(bool .ThenBy(o => o.Customer != null ? o.Customer.City : string.Empty) .ThenBy(o => o.OrderID) .Take(5), - elementAsserter: (e, a) => AssertInclude(e, a, new ExpectedInclude(o => o.OrderDetails)), - entryCount: 14); + elementAsserter: (e, a) => AssertInclude(e, a, new ExpectedInclude(o => o.OrderDetails))); } [ConditionalTheory(Skip = "SingleStore does not support this type of query: correlated subselect in ORDER BY")] @@ -62,8 +61,7 @@ from c2 in ss.Set().Include(c => c.Orders).OrderBy(c => c.CustomerID). { AssertInclude(e.c1, a.c1, new ExpectedInclude(c => c.Orders)); AssertInclude(e.c2, a.c2, new ExpectedInclude(c => c.Orders)); - }, - entryCount: 15); + }); } [ConditionalTheory(Skip = "SingleStore does not support this type of query: correlated subselect in ORDER BY")] @@ -81,8 +79,7 @@ from c2 in ss.Set().OrderBy(c => c.CustomerID).Skip(2).Take(2) { AssertInclude(e.c1, a.c1, new ExpectedInclude(c => c.Orders)); AssertEqual(e.c2, a.c2); - }, - entryCount: 8); + }); } [ConditionalTheory(Skip = "SingleStore does not support this type of query: correlated subselect in ORDER BY")] @@ -127,13 +124,13 @@ public override Task Then_include_collection_order_by_collection_column(bool asy return base.Then_include_collection_order_by_collection_column(async); } - [ConditionalTheory(Skip = "Feature 'scalar subselect inside the GROUP/ORDER BY of a pushed down query' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'scalar subselect inside the GROUP/ORDER BY of a pushed down query' is not supported by SingleStore Distributed")] public override Task Include_collection_OrderBy_empty_list_contains(bool async) { return base.Include_collection_OrderBy_empty_list_contains(async); } - [ConditionalTheory(Skip = "Feature 'scalar subselect inside the GROUP/ORDER BY of a pushed down query' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'scalar subselect inside the GROUP/ORDER BY of a pushed down query' is not supported by SingleStore Distributed")] public override Task Include_collection_OrderBy_empty_list_does_not_contains(bool async) { return base.Include_collection_OrderBy_empty_list_contains(async); @@ -148,8 +145,7 @@ public override Task Repro9735(bool async) .OrderBy(b => b.Customer.CustomerID != null) .ThenBy(b => b.Customer != null ? b.Customer.CustomerID : string.Empty) .ThenBy(b => b.EmployeeID) // Needs to be explicitly ordered by EmployeeID as well - .Take(2), - entryCount: 6); + .Take(2)); } private void AssertSql(params string[] expected) diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindJoinQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindJoinQuerySingleStoreTest.cs index be16e0666..75fbaaf99 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindJoinQuerySingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindJoinQuerySingleStoreTest.cs @@ -1,6 +1,8 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.TestUtilities; +using EntityFrameworkCore.SingleStore.FunctionalTests.TestUtilities; using Xunit; using Xunit.Abstractions; @@ -58,6 +60,22 @@ INNER JOIN ( ORDER BY `c`.`CustomerID`, `t0`.`OrderID0`, `t0`.`OrderID`"); } + // https://github.com/npgsql/efcore.pg/issues/2759 + // public override Task Join_local_collection_int_closure_is_cached_correctly(bool async) + // => Assert.ThrowsAsync(() => base.Join_local_collection_int_closure_is_cached_correctly(async)); + public override async Task Join_local_collection_int_closure_is_cached_correctly(bool async) + { + if (SingleStoreTestHelpers.HasPrimitiveCollectionsSupport(Fixture)) + { + await base.Join_local_collection_int_closure_is_cached_correctly(async); + } + else + { + await Assert.ThrowsAsync(() + => base.Join_local_collection_int_closure_is_cached_correctly(async)); + } + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindMiscellaneousQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindMiscellaneousQuerySingleStoreTest.cs index 1bd0ac2b0..f0e781070 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindMiscellaneousQuerySingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindMiscellaneousQuerySingleStoreTest.cs @@ -206,7 +206,6 @@ public override Task Entity_equality_orderby_subquery(bool async) async, ss => ss.Set().OrderBy(c => c.Orders.OrderBy(o => o.OrderID).FirstOrDefault()).ThenBy(c => c.CustomerID), ss => ss.Set().OrderBy(c => c.Orders.FirstOrDefault() == null ? (int?)null : c.Orders.OrderBy(o => o.OrderID).FirstOrDefault().OrderID).ThenBy(c => c.CustomerID), - entryCount: 91, assertOrder: true); } @@ -237,7 +236,6 @@ public override Task Checked_context_with_arithmetic_does_not_fail(bool isAsync) ss => ss.Set() .Where(w => w.Quantity + 1 == 5 && w.Quantity - 1 == 3 && w.Quantity * 1 == w.Quantity) .OrderBy(o => o.OrderID).ThenBy(o => o.ProductID), - entryCount: 55, assertOrder: true, elementAsserter: (e, a) => { AssertEqual(e, a); }); } @@ -441,6 +439,18 @@ public override Task Dependent_to_principal_navigation_equal_to_null_for_subquer return base.Dependent_to_principal_navigation_equal_to_null_for_subquery(async); } + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed.")] + public override Task Collection_navigation_equal_to_null_for_subquery_using_ElementAtOrDefault_parameter(bool async) + { + return base.Collection_navigation_equal_to_null_for_subquery_using_ElementAtOrDefault_parameter(async); + } + + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed.")] + public override Task Collection_navigation_equal_to_null_for_subquery_using_ElementAtOrDefault_constant_one(bool async) + { + return base.Collection_navigation_equal_to_null_for_subquery_using_ElementAtOrDefault_constant_one(async); + } + public override Task Where_query_composition2(bool async) { return AssertQuery( @@ -449,8 +459,7 @@ public override Task Where_query_composition2(bool async) where e1.FirstName == (from e2 in ss.Set().OrderBy(e => e.EmployeeID) select new { Foo = e2 }).First().Foo.FirstName - select e1, - entryCount: 1); + select e1); } public override Task Where_query_composition2_FirstOrDefault(bool async) @@ -461,8 +470,7 @@ public override Task Where_query_composition2_FirstOrDefault(bool async) where e1.FirstName == (from e2 in ss.Set().OrderBy(e => e.EmployeeID) select e2).FirstOrDefault().FirstName - select e1, - entryCount: 1); + select e1); } public override Task Where_query_composition2_FirstOrDefault_with_anonymous(bool async) @@ -473,8 +481,7 @@ public override Task Where_query_composition2_FirstOrDefault_with_anonymous(bool where e1.FirstName == (from e2 in ss.Set().OrderBy(e => e.EmployeeID) select new { Foo = e2 }).FirstOrDefault().Foo.FirstName - select e1, - entryCount: 1); + select e1); } /// diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindQueryFiltersQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindQueryFiltersQuerySingleStoreTest.cs index 68ac72ae5..a6be3a11a 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindQueryFiltersQuerySingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindQueryFiltersQuerySingleStoreTest.cs @@ -20,12 +20,15 @@ public override async Task Count_query(bool async) { await base.Count_query(async); - AssertSql( - @"@__ef_filter__TenantPrefix_0='B' (Size = 4000) + + AssertSql( +""" +@__ef_filter__TenantPrefix_0_rewritten='B%' (Size = 40) SELECT COUNT(*) FROM `Customers` AS `c` -WHERE (@__ef_filter__TenantPrefix_0 = '') OR (`c`.`CompanyName` IS NOT NULL AND (LEFT(`c`.`CompanyName`, CHAR_LENGTH(@__ef_filter__TenantPrefix_0)) = @__ef_filter__TenantPrefix_0))"); +WHERE `c`.`CompanyName` LIKE @__ef_filter__TenantPrefix_0_rewritten +"""); } private void AssertSql(params string[] expected) diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindQuerySingleStoreFixture.cs b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindQuerySingleStoreFixture.cs index fa0d71a68..01395f8c5 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindQuerySingleStoreFixture.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindQuerySingleStoreFixture.cs @@ -1,9 +1,11 @@ +using System; using EntityFrameworkCore.SingleStore.FunctionalTests.TestUtilities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.TestModels.Northwind; using Microsoft.EntityFrameworkCore.TestUtilities; +using EntityFrameworkCore.SingleStore.FunctionalTests.TestModels.Northwind; namespace EntityFrameworkCore.SingleStore.FunctionalTests.Query { @@ -11,6 +13,7 @@ public class NorthwindQuerySingleStoreFixture : NorthwindQuery where TModelCustomizer : IModelCustomizer, new() { protected override ITestStoreFactory TestStoreFactory => SingleStoreNorthwindTestStoreFactory.Instance; + protected override Type ContextType => typeof(NorthwindSingleStoreContext); protected override bool ShouldLogCategory(string logCategory) => logCategory == DbLoggerCategory.Query.Name || logCategory == DbLoggerCategory.Database.Command.Name; diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindSelectQuerySingleStoreTest.SingleStore.cs b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindSelectQuerySingleStoreTest.SingleStore.cs index 1fc8ab147..d692365f7 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindSelectQuerySingleStoreTest.SingleStore.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindSelectQuerySingleStoreTest.SingleStore.cs @@ -37,7 +37,8 @@ await AssertQuery( .Select(g => new {g.Key.Year, Count = g.Count()}) .Where(k => k.Year == 1995) .OrderBy(k => k.Year), - assertOrder: true); + assertOrder: true, + assertEmpty: true); // TODO: Use a linq query that does not return an empty result. AssertSql( @"SELECT `t`.`Year`, `t`.`Count` diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindSelectQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindSelectQuerySingleStoreTest.cs index c5c04dbf9..ddf82b711 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindSelectQuerySingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindSelectQuerySingleStoreTest.cs @@ -26,49 +26,49 @@ public NorthwindSelectQuerySingleStoreTest( protected override bool CanExecuteQueryString => true; - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override Task Client_projection_with_string_initialization_with_scalar_subquery(bool async) { return base.Client_projection_with_string_initialization_with_scalar_subquery(async); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override Task Collection_FirstOrDefault_with_nullable_unsigned_int_column(bool async) { return base.Collection_FirstOrDefault_with_nullable_unsigned_int_column(async); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override Task FirstOrDefault_over_empty_collection_of_value_type_returns_correct_results(bool async) { return base.FirstOrDefault_over_empty_collection_of_value_type_returns_correct_results(async); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override Task LastOrDefault_member_access_in_projection_translates_to_server(bool async) { return base.LastOrDefault_member_access_in_projection_translates_to_server(async); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override Task Project_single_element_from_collection_with_OrderBy_Distinct_and_FirstOrDefault(bool async) { return base.Project_single_element_from_collection_with_OrderBy_Distinct_and_FirstOrDefault(async); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override Task Project_single_element_from_collection_with_OrderBy_Skip_and_FirstOrDefault(bool async) { return base.Project_single_element_from_collection_with_OrderBy_Skip_and_FirstOrDefault(async); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override Task Project_uint_through_collection_FirstOrDefault(bool async) { return base.Project_uint_through_collection_FirstOrDefault(async); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override Task Projecting_Length_of_a_string_property_after_FirstOrDefault_on_correlated_collection(bool async) { return base.Projecting_Length_of_a_string_property_after_FirstOrDefault_on_correlated_collection(async); @@ -80,13 +80,13 @@ public override Task Projection_AsEnumerable_projection(bool async) return base.Projection_AsEnumerable_projection(async); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override Task Select_nested_collection_multi_level2(bool async) { return base.Select_nested_collection_multi_level2(async); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override Task Select_nested_collection_multi_level3(bool async) { return base.Select_nested_collection_multi_level2(async); @@ -196,14 +196,72 @@ public override async Task Select_datetime_millisecond_component(bool async) FROM `Orders` AS `o`"); } + [SupportedServerVersionCondition(nameof(ServerVersionSupport.JsonTableImplementationUsingParameterAsSourceWithoutEngineCrash), Skip = "This test/query crashes MySQL 8 even with inlined parameters every single time. Could be related to LATERAL.")] + public override async Task Correlated_collection_after_distinct_not_containing_original_identifier(bool async) + { + await base.Correlated_collection_after_distinct_not_containing_original_identifier(async); + + AssertSql( +""" +SELECT `t`.`OrderDate`, `t`.`CustomerID`, `t0`.`Outer1`, `t0`.`Outer2`, `t0`.`Inner`, `t0`.`OrderDate` +FROM ( + SELECT DISTINCT `o`.`OrderDate`, `o`.`CustomerID` + FROM `Orders` AS `o` +) AS `t` +LEFT JOIN LATERAL ( + SELECT `t`.`OrderDate` AS `Outer1`, `t`.`CustomerID` AS `Outer2`, `o0`.`OrderID` AS `Inner`, `o0`.`OrderDate` + FROM `Orders` AS `o0` + WHERE ((`o0`.`CustomerID` = `t`.`CustomerID`) OR (`o0`.`CustomerID` IS NULL AND (`t`.`CustomerID` IS NULL))) AND `o0`.`OrderID` IN ( + SELECT `f`.`value` + FROM JSON_TABLE('[10248,10249,10250]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `f` + ) +) AS `t0` ON TRUE +ORDER BY `t`.`OrderDate`, `t`.`CustomerID` +"""); + } + + [SupportedServerVersionCondition(nameof(ServerVersionSupport.JsonTableImplementationUsingParameterAsSourceWithoutEngineCrash), Skip = "This test/query crashes MySQL 8 even with inlined parameters every single time. Could be related to LATERAL.")] + public override async Task Correlated_collection_after_groupby_with_complex_projection_not_containing_original_identifier(bool async) + { + await base.Correlated_collection_after_groupby_with_complex_projection_not_containing_original_identifier(async); + + AssertSql( +""" + SELECT `t0`.`CustomerID`, `t0`.`Complex`, `t1`.`Outer`, `t1`.`Inner`, `t1`.`OrderDate` + FROM ( + SELECT `t`.`CustomerID`, `t`.`Complex` + FROM ( + SELECT `o`.`CustomerID`, EXTRACT(month FROM `o`.`OrderDate`) AS `Complex` + FROM `Orders` AS `o` + ) AS `t` + GROUP BY `t`.`CustomerID`, `t`.`Complex` + ) AS `t0` + LEFT JOIN LATERAL ( + SELECT `t0`.`CustomerID` AS `Outer`, `o0`.`OrderID` AS `Inner`, `o0`.`OrderDate` + FROM `Orders` AS `o0` + WHERE ((`o0`.`CustomerID` = `t0`.`CustomerID`) OR (`o0`.`CustomerID` IS NULL AND (`t0`.`CustomerID` IS NULL))) AND `o0`.`OrderID` IN ( + SELECT `f`.`value` + FROM JSON_TABLE('[10248,10249,10250]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `f` + ) + ) AS `t1` ON TRUE + ORDER BY `t0`.`CustomerID`, `t0`.`Complex` +"""); + } + public override async Task Correlated_collection_after_distinct_with_complex_projection_not_containing_original_identifier(bool async) { // Identifier set for Distinct. Issue #24440. - Assert.Equal( - RelationalStrings.InsufficientInformationToIdentifyElementOfCollectionJoin, - (await Assert.ThrowsAsync( + var message = (await Assert.ThrowsAsync( () => base.Correlated_collection_after_distinct_with_complex_projection_not_containing_original_identifier(async))) - .Message); + .Message; + + Assert.Equal(RelationalStrings.InsufficientInformationToIdentifyElementOfCollectionJoin, message); AssertSql(); } diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindSetOperationsQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindSetOperationsQuerySingleStoreTest.cs index 962394f96..a94b38aa3 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindSetOperationsQuerySingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindSetOperationsQuerySingleStoreTest.cs @@ -202,14 +202,16 @@ public override async Task Union_Select_scalar(bool async) await base.Union_Select_scalar(async); AssertSql( - @"SELECT 1 +""" +SELECT 1 FROM ( SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` - EXCEPT + UNION SELECT `c0`.`CustomerID`, `c0`.`Address`, `c0`.`City`, `c0`.`CompanyName`, `c0`.`ContactName`, `c0`.`ContactTitle`, `c0`.`Country`, `c0`.`Fax`, `c0`.`Phone`, `c0`.`PostalCode`, `c0`.`Region` FROM `Customers` AS `c0` -) AS `t`"); +) AS `t` +"""); } private void AssertSql(params string[] expected) diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindSplitIncludeNoTrackingQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindSplitIncludeNoTrackingQuerySingleStoreTest.cs index f59fcbe13..87ff1d597 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindSplitIncludeNoTrackingQuerySingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindSplitIncludeNoTrackingQuerySingleStoreTest.cs @@ -33,8 +33,7 @@ from c2 in ss.Set().OrderBy(c => c.CustomerID).Skip(2).Take(2) { AssertInclude(e.c1, a.c1, new ExpectedInclude(c => c.Orders)); AssertEqual(e.c2, a.c2); - }, - entryCount: 8); + }); } [ConditionalTheory(Skip = "SingleStore does not support this type of query: correlated subselect in ORDER BY")] @@ -61,7 +60,7 @@ public override Task Then_include_collection_order_by_collection_column(bool asy return base.Then_include_collection_order_by_collection_column(async); } - [ConditionalTheory(Skip = "Feature 'scalar subselect inside the GROUP/ORDER BY of a pushed down query' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'scalar subselect inside the GROUP/ORDER BY of a pushed down query' is not supported by SingleStore Distributed")] public override Task Include_collection_OrderBy_empty_list_contains(bool async) { return base.Include_collection_OrderBy_empty_list_contains(async); @@ -82,8 +81,7 @@ public override Task Repro9735(bool async) .OrderBy(b => b.Customer.CustomerID != null) .ThenBy(b => b.Customer != null ? b.Customer.CustomerID : string.Empty) .ThenBy(b => b.EmployeeID) // Needs to be explicitly ordered by EmployeeID as well - .Take(2), - entryCount: 6); + .Take(2)); } public override Task Include_collection_with_multiple_conditional_order_by(bool async) @@ -96,8 +94,7 @@ public override Task Include_collection_with_multiple_conditional_order_by(bool .ThenBy(o => o.Customer != null ? o.Customer.City : string.Empty) .ThenBy(b => b.EmployeeID) // Needs to be explicitly ordered by EmployeeID as well .Take(5), - elementAsserter: (e, a) => AssertInclude(e, a, new ExpectedInclude(o => o.OrderDetails)), - entryCount: 14); + elementAsserter: (e, a) => AssertInclude(e, a, new ExpectedInclude(o => o.OrderDetails))); } [ConditionalTheory(Skip = "https://github.com/dotnet/efcore/issues/21202")] diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindSplitIncludeQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindSplitIncludeQuerySingleStoreTest.cs index 35170a43d..ecb38a866 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindSplitIncludeQuerySingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindSplitIncludeQuerySingleStoreTest.cs @@ -31,8 +31,7 @@ public override Task Include_collection_with_multiple_conditional_order_by(bool .ThenBy(o => o.Customer != null ? o.Customer.City : string.Empty) .ThenBy(o => o.OrderID) .Take(5), - elementAsserter: (e, a) => AssertInclude(e, a, new ExpectedInclude(o => o.OrderDetails)), - entryCount: 14); + elementAsserter: (e, a) => AssertInclude(e, a, new ExpectedInclude(o => o.OrderDetails))); } [ConditionalTheory(Skip = "Further investigation is needed to determine why it is failing with SingleStore")] @@ -66,8 +65,7 @@ public override Task SelectMany_Include_collection_GroupBy_Select(bool async) .Where(od => od.ProductID == 72) // <-- explicit filtering needed for some MariaDB versions, because we cannot manually influence the order of the Order.OrderDetails property from o in ss.Set().Include(o => o.OrderDetails) select o) .GroupBy(e => e.OrderID) - .Select(e => e.OrderBy(o => o.OrderID).FirstOrDefault()), - entryCount: 2985); + .Select(e => e.OrderBy(o => o.OrderID).FirstOrDefault())); } [ConditionalTheory(Skip = "SingleStore does not support this type of query: correlated subselect in ORDER BY")] @@ -85,8 +83,7 @@ from c2 in ss.Set().Include(c => c.Orders).OrderBy(c => c.CustomerID). { AssertInclude(e.c1, a.c1, new ExpectedInclude(c => c.Orders)); AssertInclude(e.c2, a.c2, new ExpectedInclude(c => c.Orders)); - }, - entryCount: 15); + }); } [ConditionalTheory(Skip = "SingleStore does not support this type of query: correlated subselect in ORDER BY")] @@ -105,8 +102,7 @@ from c2 in ss.Set().OrderBy(c => c.CustomerID).Skip(2).Take(2) { AssertInclude(e.c1, a.c1, new ExpectedInclude(c => c.Orders)); AssertEqual(e.c2, a.c2); - }, - entryCount: 8); + }); } [ConditionalTheory(Skip = "https://github.com/dotnet/efcore/issues/21202")] @@ -151,13 +147,13 @@ public override Task Include_collection_order_by_subquery(bool async) return base.Include_collection_order_by_subquery(async); } - [ConditionalTheory(Skip = "Feature 'scalar subselect inside the GROUP/ORDER BY of a pushed down query' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'scalar subselect inside the GROUP/ORDER BY of a pushed down query' is not supported by SingleStore Distributed")] public override Task Include_collection_OrderBy_empty_list_contains(bool async) { return base.Include_collection_OrderBy_empty_list_contains(async); } - [ConditionalTheory(Skip = "Feature 'scalar subselect inside the GROUP/ORDER BY of a pushed down query' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'scalar subselect inside the GROUP/ORDER BY of a pushed down query' is not supported by SingleStore Distributed")] public override Task Include_collection_OrderBy_empty_list_does_not_contains(bool async) { return base.Include_collection_OrderBy_empty_list_does_not_contains(async); @@ -172,8 +168,7 @@ public override Task Repro9735(bool async) .OrderBy(b => b.Customer.CustomerID != null) .ThenBy(b => b.Customer != null ? b.Customer.CustomerID : string.Empty) .ThenBy(b => b.EmployeeID) // Needs to be explicitly ordered by EmployeeID as well - .Take(2), - entryCount: 6); + .Take(2)); } private void AssertSql(params string[] expected) diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindSqlQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindSqlQuerySingleStoreTest.cs index dfe4bcee1..28792a8dc 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindSqlQuerySingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindSqlQuerySingleStoreTest.cs @@ -35,16 +35,16 @@ public override async Task SqlQuery_composed_Contains(bool async) await base.SqlQuery_composed_Contains(async); AssertSql( - """ - SELECT `o`.`OrderID`, `o`.`CustomerID`, `o`.`EmployeeID`, `o`.`OrderDate` - FROM `Orders` AS `o` - WHERE EXISTS ( - SELECT 1 - FROM ( - SELECT `ProductID` AS `Value` FROM `Products` - ) AS `t` - WHERE CAST(`t`.`Value` AS signed) = `o`.`OrderID`) - """); +""" +SELECT `o`.`OrderID`, `o`.`CustomerID`, `o`.`EmployeeID`, `o`.`OrderDate` +FROM `Orders` AS `o` +WHERE `o`.`OrderID` IN ( + SELECT `t`.`Value` + FROM ( + SELECT `ProductID` AS `Value` FROM `Products` + ) AS `t` +) +"""); } public override async Task SqlQuery_composed_Join(bool async) diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindStringIncludeQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindStringIncludeQuerySingleStoreTest.cs index a7a6f687e..07ebcd250 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindStringIncludeQuerySingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindStringIncludeQuerySingleStoreTest.cs @@ -92,8 +92,7 @@ public override Task Include_collection_with_multiple_conditional_order_by(bool .ThenBy(o => o.Customer != null ? o.Customer.City : string.Empty) .ThenBy(o => o.OrderID) .Take(5), - elementAsserter: (e, a) => AssertInclude(e, a, new ExpectedInclude(o => o.OrderDetails)), - entryCount: 14); + elementAsserter: (e, a) => AssertInclude(e, a, new ExpectedInclude(o => o.OrderDetails))); } [ConditionalTheory(Skip = "SingleStore does not support this type of query: correlated subselect in ORDER BY")] @@ -111,8 +110,7 @@ from c2 in ss.Set().Include(c => c.Orders).OrderBy(c => c.CustomerID). { AssertInclude(e.c1, a.c1, new ExpectedInclude(c => c.Orders)); AssertInclude(e.c2, a.c2, new ExpectedInclude(c => c.Orders)); - }, - entryCount: 15); + }); } [ConditionalTheory(Skip = "SingleStore does not support this type of query: correlated subselect in ORDER BY")] @@ -131,8 +129,7 @@ from c2 in ss.Set().OrderBy(c => c.CustomerID).Skip(2).Take(2) { AssertInclude(e.c1, a.c1, new ExpectedInclude(c => c.Orders)); AssertEqual(e.c2, a.c2); - }, - entryCount: 8); + }); } public override Task Repro9735(bool async) @@ -144,8 +141,7 @@ public override Task Repro9735(bool async) .OrderBy(b => b.Customer.CustomerID != null) .ThenBy(b => b.Customer != null ? b.Customer.CustomerID : string.Empty) .ThenBy(b => b.EmployeeID) // Needs to be explicitly ordered by EmployeeID as well - .Take(2), - entryCount: 6); + .Take(2)); } private void AssertSql(params string[] expected) diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindWhereQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindWhereQuerySingleStoreTest.cs index 307c3e111..d65df05ed 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindWhereQuerySingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/NorthwindWhereQuerySingleStoreTest.cs @@ -143,9 +143,11 @@ public override async Task Where_datetime_hour_component(bool async) await base.Where_datetime_hour_component(async); AssertSql( - @"SELECT `o`.`OrderID`, `o`.`CustomerID`, `o`.`EmployeeID`, `o`.`OrderDate` +""" +SELECT `o`.`OrderID`, `o`.`CustomerID`, `o`.`EmployeeID`, `o`.`OrderDate` FROM `Orders` AS `o` -WHERE EXTRACT(hour FROM `o`.`OrderDate`) = 14"); +WHERE EXTRACT(hour FROM `o`.`OrderDate`) = 0 +"""); } [ConditionalTheory] @@ -154,9 +156,11 @@ public override async Task Where_datetime_minute_component(bool async) await base.Where_datetime_minute_component(async); AssertSql( - @"SELECT `o`.`OrderID`, `o`.`CustomerID`, `o`.`EmployeeID`, `o`.`OrderDate` +""" +SELECT `o`.`OrderID`, `o`.`CustomerID`, `o`.`EmployeeID`, `o`.`OrderDate` FROM `Orders` AS `o` -WHERE EXTRACT(minute FROM `o`.`OrderDate`) = 23"); +WHERE EXTRACT(minute FROM `o`.`OrderDate`) = 0 +"""); } [ConditionalTheory] @@ -165,9 +169,11 @@ public override async Task Where_datetime_second_component(bool async) await base.Where_datetime_second_component(async); AssertSql( - @"SELECT `o`.`OrderID`, `o`.`CustomerID`, `o`.`EmployeeID`, `o`.`OrderDate` +""" +SELECT `o`.`OrderID`, `o`.`CustomerID`, `o`.`EmployeeID`, `o`.`OrderDate` FROM `Orders` AS `o` -WHERE EXTRACT(second FROM `o`.`OrderDate`) = 44"); +WHERE EXTRACT(second FROM `o`.`OrderDate`) = 0 +"""); } [ConditionalTheory] @@ -176,9 +182,11 @@ public override async Task Where_datetime_millisecond_component(bool async) await base.Where_datetime_millisecond_component(async); AssertSql( - @"SELECT `o`.`OrderID`, `o`.`CustomerID`, `o`.`EmployeeID`, `o`.`OrderDate` +""" +SELECT `o`.`OrderID`, `o`.`CustomerID`, `o`.`EmployeeID`, `o`.`OrderDate` FROM `Orders` AS `o` -WHERE (EXTRACT(microsecond FROM `o`.`OrderDate`)) DIV (1000) = 88"); +WHERE (EXTRACT(microsecond FROM `o`.`OrderDate`)) DIV (1000) = 0 +"""); } [ConditionalTheory] @@ -248,8 +256,7 @@ public virtual async Task Where_string_remove(bool async) { await AssertQuery( async, - ss => ss.Set().Where(c => c.City.Remove(3) == "Sea"), - entryCount: 1); + ss => ss.Set().Where(c => c.City.Remove(3) == "Sea")); AssertSql( @"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` @@ -263,8 +270,7 @@ public virtual async Task Where_string_remove_count(bool async) { await AssertQuery( async, - ss => ss.Set().Where(c => c.City.Remove(3, 1) == "Seatle"), - entryCount: 1); + ss => ss.Set().Where(c => c.City.Remove(3, 1) == "Seatle")); AssertSql( @"SELECT `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` @@ -281,7 +287,7 @@ public virtual async Task Where_guid(bool async) await AssertQuery( async, ss => ss.Set().Where(c => guidParameter == Guid.NewGuid()), - entryCount: 0); + assertEmpty: true); AssertSql( @"@__guidParameter_0='4d68fe70-ddb0-47d7-b6db-437684fa3e1f' @@ -296,12 +302,14 @@ public override async Task Where_string_concat_method_comparison_2(bool async) await base.Where_string_concat_method_comparison_2(async); AssertSql( - @"@__i_0='A' (Size = 4000) +""" +@__i_0='A' (Size = 4000) @__j_1='B' (Size = 4000) SELECT `c`.`CustomerID` FROM `Customers` AS `c` -WHERE CONCAT(@__i_0, @__j_1, `c`.`CustomerID`) = `c`.`CompanyName`"); +WHERE CONCAT(@__i_0, @__j_1, `c`.`CustomerID`) = 'ABANATR' +"""); } public override async Task Where_string_concat_method_comparison_3(bool async) @@ -309,13 +317,15 @@ public override async Task Where_string_concat_method_comparison_3(bool async) await base.Where_string_concat_method_comparison_3(async); AssertSql( - @"@__i_0='A' (Size = 4000) +""" +@__i_0='A' (Size = 4000) @__j_1='B' (Size = 4000) @__k_2='C' (Size = 4000) SELECT `c`.`CustomerID` FROM `Customers` AS `c` -WHERE CONCAT(@__i_0, @__j_1, @__k_2, `c`.`CustomerID`) = `c`.`CompanyName`"); +WHERE CONCAT(@__i_0, @__j_1, @__k_2, `c`.`CustomerID`) = 'ABCANTON' +"""); } [ConditionalTheory] @@ -326,14 +336,17 @@ public virtual async Task Where_string_concat_method_comparison_single_object(bo await AssertQuery( async, - ss => ss.Set().Where(c => string.Concat(i) == c.CompanyName).Select(c => c.CustomerID)); + ss => ss.Set().Where(c => string.Concat(i) == c.CompanyName).Select(c => c.CustomerID), + assertEmpty: true); AssertSql( - @"@__Concat_0='1' (Size = 4000) +""" +@__Concat_0='1' (Size = 40) SELECT `c`.`CustomerID` FROM `Customers` AS `c` -WHERE @__Concat_0 = `c`.`CompanyName`"); +WHERE @__Concat_0 = `c`.`CompanyName` +"""); } [ConditionalTheory] @@ -344,7 +357,8 @@ public virtual async Task Where_string_concat_method_comparison_object(bool asyn await AssertQuery( async, - ss => ss.Set().Where(c => string.Concat(i, c.CustomerID) == c.CompanyName).Select(c => c.CustomerID)); + ss => ss.Set().Where(c => string.Concat(i, c.CustomerID) == c.CompanyName).Select(c => c.CustomerID), + assertEmpty: true); AssertSql( @"@__i_0='1' (Size = 4000) @@ -363,7 +377,8 @@ public virtual async Task Where_string_concat_method_comparison_object_2(bool as await AssertQuery( async, - ss => ss.Set().Where(c => string.Concat(i, j, c.CustomerID) == c.CompanyName).Select(c => c.CustomerID)); + ss => ss.Set().Where(c => string.Concat(i, j, c.CustomerID) == c.CompanyName).Select(c => c.CustomerID), + assertEmpty: true); AssertSql( @"@__i_0='1' (Size = 4000) @@ -384,16 +399,19 @@ public virtual async Task Where_string_concat_method_comparison_object_3(bool as await AssertQuery( async, - ss => ss.Set().Where(c => string.Concat(i, j, k, c.CustomerID) == c.CompanyName).Select(c => c.CustomerID)); + ss => ss.Set().Where(c => string.Concat(i, j, k, c.CustomerID) == c.CompanyName).Select(c => c.CustomerID), + assertEmpty: true); AssertSql( - @"@__i_0='1' (Size = 4000) +""" +@__i_0='1' (Size = 4000) @__j_1='2' (Size = 4000) @__k_2='3' (Size = 4000) SELECT `c`.`CustomerID` FROM `Customers` AS `c` -WHERE (CONCAT(@__i_0, @__j_1, @__k_2, `c`.`CustomerID`) = `c`.`CompanyName`) OR (CONCAT(@__i_0, @__j_1, @__k_2, `c`.`CustomerID`) IS NULL AND (`c`.`CompanyName` IS NULL))"); +WHERE CONCAT(@__i_0, @__j_1, @__k_2, `c`.`CustomerID`) = `c`.`CompanyName` +"""); } [ConditionalTheory] @@ -407,17 +425,20 @@ public virtual async Task Where_string_concat_method_comparison_params_string_ar await AssertQuery( async, - ss => ss.Set().Where(c => string.Concat(i, j, k, m, c.CustomerID) == c.CompanyName).Select(c => c.CustomerID)); + ss => ss.Set().Where(c => string.Concat(i, j, k, m, c.CustomerID) == c.CompanyName).Select(c => c.CustomerID), + assertEmpty: true); AssertSql( - @"@__i_0='A' (Size = 4000) +""" +@__i_0='A' (Size = 4000) @__j_1='B' (Size = 4000) @__k_2='C' (Size = 4000) @__m_3='D' (Size = 4000) SELECT `c`.`CustomerID` FROM `Customers` AS `c` -WHERE (CONCAT(@__i_0, @__j_1, @__k_2, @__m_3, `c`.`CustomerID`) = `c`.`CompanyName`) OR (CONCAT(@__i_0, @__j_1, @__k_2, @__m_3, `c`.`CustomerID`) IS NULL AND (`c`.`CompanyName` IS NULL))"); +WHERE CONCAT(@__i_0, @__j_1, @__k_2, @__m_3, `c`.`CustomerID`) = `c`.`CompanyName` +"""); } [ConditionalTheory] @@ -428,14 +449,17 @@ public virtual async Task Where_string_concat_method_comparison_explicit_string_ await AssertQuery( async, - ss => ss.Set().Where(c => string.Concat(array) == c.CompanyName).Select(c => c.CustomerID)); + ss => ss.Set().Where(c => string.Concat(array) == c.CompanyName).Select(c => c.CustomerID), + assertEmpty: true); AssertSql( - @"@__Concat_0='ABCD' (Size = 4000) +""" +@__Concat_0='ABCD' (Size = 40) SELECT `c`.`CustomerID` FROM `Customers` AS `c` -WHERE @__Concat_0 = `c`.`CompanyName`"); +WHERE @__Concat_0 = `c`.`CompanyName` +"""); } [ConditionalTheory] @@ -446,14 +470,17 @@ public virtual async Task Where_string_concat_method_comparison_explicit_string_ await AssertQuery( async, - ss => ss.Set().Where(c => string.Concat(array) == c.CompanyName).Select(c => c.CustomerID)); + ss => ss.Set().Where(c => string.Concat(array) == c.CompanyName).Select(c => c.CustomerID), + assertEmpty: true); AssertSql( - @"@__Concat_0='A' (Size = 4000) +""" +@__Concat_0='A' (Size = 40) SELECT `c`.`CustomerID` FROM `Customers` AS `c` -WHERE @__Concat_0 = `c`.`CompanyName`"); +WHERE @__Concat_0 = `c`.`CompanyName` +"""); } [ConditionalTheory] @@ -467,17 +494,20 @@ public virtual async Task Where_string_concat_method_comparison_params_object_ar await AssertQuery( async, - ss => ss.Set().Where(c => string.Concat(i, j, k, m, c.CustomerID) == c.CompanyName).Select(c => c.CustomerID)); + ss => ss.Set().Where(c => string.Concat(i, j, k, m, c.CustomerID) == c.CompanyName).Select(c => c.CustomerID), + assertEmpty: true); AssertSql( - @"@__i_0='1' (Size = 4000) +""" +@__i_0='1' (Size = 4000) @__j_1='2' (Size = 4000) @__k_2='3' (Size = 4000) @__m_3='4' (Size = 4000) SELECT `c`.`CustomerID` FROM `Customers` AS `c` -WHERE (CONCAT(@__i_0, @__j_1, @__k_2, @__m_3, `c`.`CustomerID`) = `c`.`CompanyName`) OR (CONCAT(@__i_0, @__j_1, @__k_2, @__m_3, `c`.`CustomerID`) IS NULL AND (`c`.`CompanyName` IS NULL))"); +WHERE CONCAT(@__i_0, @__j_1, @__k_2, @__m_3, `c`.`CustomerID`) = `c`.`CompanyName` +"""); } [ConditionalTheory] @@ -488,14 +518,17 @@ public virtual async Task Where_string_concat_method_comparison_explicit_object_ await AssertQuery( async, - ss => ss.Set().Where(c => string.Concat(array) == c.CompanyName).Select(c => c.CustomerID)); + ss => ss.Set().Where(c => string.Concat(array) == c.CompanyName).Select(c => c.CustomerID), + assertEmpty: true); AssertSql( - @"@__Concat_0='1234' (Size = 4000) +""" +@__Concat_0='1234' (Size = 40) SELECT `c`.`CustomerID` FROM `Customers` AS `c` -WHERE @__Concat_0 = `c`.`CompanyName`"); +WHERE @__Concat_0 = `c`.`CompanyName` +"""); } [ConditionalTheory] @@ -506,14 +539,17 @@ public virtual async Task Where_string_concat_method_comparison_explicit_object_ await AssertQuery( async, - ss => ss.Set().Where(c => string.Concat(array) == c.CompanyName).Select(c => c.CustomerID)); + ss => ss.Set().Where(c => string.Concat(array) == c.CompanyName).Select(c => c.CustomerID), + assertEmpty: true); AssertSql( - @"@__Concat_0='1' (Size = 4000) +""" +@__Concat_0='1' (Size = 40) SELECT `c`.`CustomerID` FROM `Customers` AS `c` -WHERE @__Concat_0 = `c`.`CompanyName`"); +WHERE @__Concat_0 = `c`.`CompanyName` +"""); } [ConditionalTheory] @@ -524,14 +560,17 @@ public virtual async Task Where_string_concat_method_comparison_string_enumerabl await AssertQuery( async, - ss => ss.Set().Where(c => string.Concat(enumerable) == c.CompanyName).Select(c => c.CustomerID)); + ss => ss.Set().Where(c => string.Concat(enumerable) == c.CompanyName).Select(c => c.CustomerID), + assertEmpty: true); AssertSql( - @"@__Concat_0='ABCD' (Size = 4000) +""" +@__Concat_0='ABCD' (Size = 40) SELECT `c`.`CustomerID` FROM `Customers` AS `c` -WHERE @__Concat_0 = `c`.`CompanyName`"); +WHERE @__Concat_0 = `c`.`CompanyName` +"""); } [ConditionalTheory] @@ -542,14 +581,17 @@ public virtual async Task Where_string_concat_method_comparison_string_enumerabl await AssertQuery( async, - ss => ss.Set().Where(c => string.Concat(enumerable) == c.CompanyName).Select(c => c.CustomerID)); + ss => ss.Set().Where(c => string.Concat(enumerable) == c.CompanyName).Select(c => c.CustomerID), + assertEmpty: true); AssertSql( - @"@__Concat_0='A' (Size = 4000) +""" +@__Concat_0='A' (Size = 40) SELECT `c`.`CustomerID` FROM `Customers` AS `c` -WHERE @__Concat_0 = `c`.`CompanyName`"); +WHERE @__Concat_0 = `c`.`CompanyName` +"""); } [ConditionalTheory] @@ -560,14 +602,17 @@ public virtual async Task Where_string_concat_method_comparison_generic_enumerab await AssertQuery( async, - ss => ss.Set().Where(c => string.Concat(enumerable) == c.CompanyName).Select(c => c.CustomerID)); + ss => ss.Set().Where(c => string.Concat(enumerable) == c.CompanyName).Select(c => c.CustomerID), + assertEmpty: true); AssertSql( - @"@__Concat_0='1234' (Size = 4000) +""" +@__Concat_0='1234' (Size = 40) SELECT `c`.`CustomerID` FROM `Customers` AS `c` -WHERE @__Concat_0 = `c`.`CompanyName`"); +WHERE @__Concat_0 = `c`.`CompanyName` +"""); } [ConditionalTheory] @@ -578,22 +623,45 @@ public virtual async Task Where_string_concat_method_comparison_generic_enumerab await AssertQuery( async, - ss => ss.Set().Where(c => string.Concat(enumerable) == c.CompanyName).Select(c => c.CustomerID)); + ss => ss.Set().Where(c => string.Concat(enumerable) == c.CompanyName).Select(c => c.CustomerID), + assertEmpty: true); AssertSql( - @"@__Concat_0='1' (Size = 4000) +""" +@__Concat_0='1' (Size = 40) SELECT `c`.`CustomerID` FROM `Customers` AS `c` -WHERE @__Concat_0 = `c`.`CompanyName`"); +WHERE @__Concat_0 = `c`.`CompanyName` +"""); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed.")] public override Task Where_subquery_FirstOrDefault_compared_to_entity(bool async) { return base.Where_subquery_FirstOrDefault_compared_to_entity(async); } + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed.")] + public override Task ElementAt_over_custom_projection_compared_to_not_null(bool async) + { + return base.ElementAt_over_custom_projection_compared_to_not_null(async); + } + + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed.")] + [MemberData(nameof(IsAsyncData))] + public override Task Where_Queryable_AsEnumerable_Contains_negated(bool async) + { + return base.Where_Queryable_AsEnumerable_Contains_negated(async); + } + + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed.")] + [MemberData(nameof(IsAsyncData))] + public override Task ElementAtOrDefault_over_custom_projection_compared_to_null(bool async) + { + return base.ElementAtOrDefault_over_custom_projection_compared_to_null(async); + } + public override async Task Where_bitwise_xor(bool async) { // Cannot eval 'where (([c].CustomerID == \"ALFKI\") ^ True)'. Issue #16645. diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/NullSemanticsQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/NullSemanticsQuerySingleStoreTest.cs index 9e1ccf68f..4ce3c30e1 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/NullSemanticsQuerySingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/NullSemanticsQuerySingleStoreTest.cs @@ -59,6 +59,13 @@ await AssertQueryScalar( WHERE @__prm_0 = (`e`.`NullableBoolC` IS NOT NULL)"); } + [ConditionalTheory(Skip = "LIKE ... ESCAPE is not supported by SingleStore Distributed.")] + public override async Task Like_with_escape_char(bool async) + { + await base.Like_with_escape_char(async); + } + + protected override NullSemanticsContext CreateContext(bool useRelationalNulls = false) { var options = new DbContextOptionsBuilder(Fixture.CreateOptions()); diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/OperatorsProceduralSingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/OperatorsProceduralSingleStoreTest.cs new file mode 100644 index 000000000..5a47b19f2 --- /dev/null +++ b/test/EFCore.SingleStore.FunctionalTests/Query/OperatorsProceduralSingleStoreTest.cs @@ -0,0 +1,11 @@ +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.TestUtilities; +using EntityFrameworkCore.SingleStore.FunctionalTests.TestUtilities; + +namespace EntityFrameworkCore.SingleStore.FunctionalTests.Query; + +public class OperatorsProceduralSingleStoreTest : OperatorsProceduralQueryTestBase +{ + protected override ITestStoreFactory TestStoreFactory + => SingleStoreTestStoreFactory.Instance; +} diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/OperatorsQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/OperatorsQuerySingleStoreTest.cs new file mode 100644 index 000000000..0690a9842 --- /dev/null +++ b/test/EFCore.SingleStore.FunctionalTests/Query/OperatorsQuerySingleStoreTest.cs @@ -0,0 +1,135 @@ +using System; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.TestModels.Operators; +using Microsoft.EntityFrameworkCore.TestUtilities; +using EntityFrameworkCore.SingleStore.FunctionalTests.TestUtilities; +using Xunit; + +namespace EntityFrameworkCore.SingleStore.FunctionalTests.Query; + +public class OperatorsQuerySingleStoreTest : OperatorsQueryTestBase +{ + protected override ITestStoreFactory TestStoreFactory + => SingleStoreTestStoreFactory.Instance; + + protected void AssertSql(params string[] expected) + => TestSqlLoggerFactory.AssertBaseline(expected); + + public override async Task Bitwise_and_on_expression_with_like_and_null_check_being_compared_to_false() + { + await base.Bitwise_and_on_expression_with_like_and_null_check_being_compared_to_false(); + + AssertSql(""); + } + + public override async Task Complex_predicate_with_bitwise_and_modulo_and_negation() + { + await base.Complex_predicate_with_bitwise_and_modulo_and_negation(); + + AssertSql(""); + } + + public override async Task Complex_predicate_with_bitwise_and_arithmetic_operations() + { + await base.Complex_predicate_with_bitwise_and_arithmetic_operations(); + + AssertSql(""); + } + + public override async Task Or_on_two_nested_binaries_and_another_simple_comparison() + { + await base.Or_on_two_nested_binaries_and_another_simple_comparison(); + + AssertSql( +""" +SELECT `o`.`Id` AS `Id1`, `o0`.`Id` AS `Id2`, `o1`.`Id` AS `Id3`, `o2`.`Id` AS `Id4`, `o3`.`Id` AS `Id5` +FROM `OperatorEntityString` AS `o` +CROSS JOIN `OperatorEntityString` AS `o0` +CROSS JOIN `OperatorEntityString` AS `o1` +CROSS JOIN `OperatorEntityString` AS `o2` +CROSS JOIN `OperatorEntityInt` AS `o3` +WHERE ((((`o`.`Value` = 'A') AND `o`.`Value` IS NOT NULL) AND ((`o0`.`Value` = 'A') AND `o0`.`Value` IS NOT NULL)) | (((`o1`.`Value` = 'B') AND `o1`.`Value` IS NOT NULL) AND ((`o2`.`Value` = 'B') AND `o2`.`Value` IS NOT NULL))) AND (`o3`.`Value` = 2) +ORDER BY `o`.`Id`, `o0`.`Id`, `o1`.`Id`, `o2`.`Id`, `o3`.`Id` +"""); + } + + public override async Task Projection_with_not_and_negation_on_integer() + { + await base.Projection_with_not_and_negation_on_integer(); + + AssertSql( +""" +SELECT CAST(~-(-((`o1`.`Value` + `o`.`Value`) + 2)) AS signed) % (-(`o0`.`Value` + `o0`.`Value`) - `o`.`Value`) +FROM `OperatorEntityLong` AS `o` +CROSS JOIN `OperatorEntityLong` AS `o0` +CROSS JOIN `OperatorEntityLong` AS `o1` +ORDER BY `o`.`Id`, `o0`.`Id`, `o1`.`Id` +"""); + } + + public override async Task Negate_on_column(bool async) + { + await base.Negate_on_column(async); + + AssertSql( +""" +SELECT `o`.`Id` +FROM `OperatorEntityInt` AS `o` +WHERE `o`.`Id` = -`o`.`Value` +"""); + } + + public override async Task Double_negate_on_column() + { + await base.Double_negate_on_column(); + + AssertSql( +""" +SELECT `o`.`Id` +FROM `OperatorEntityInt` AS `o` +WHERE -(-`o`.`Value`) = `o`.`Value` +"""); + } + + public override async Task Negate_on_binary_expression(bool async) + { + await base.Negate_on_binary_expression(async); + + AssertSql( +""" +SELECT `o`.`Id` AS `Id1`, `o0`.`Id` AS `Id2` +FROM `OperatorEntityInt` AS `o` +CROSS JOIN `OperatorEntityInt` AS `o0` +WHERE -`o`.`Value` = -(`o`.`Id` + `o0`.`Value`) +"""); + } + + public override async Task Negate_on_like_expression(bool async) + { + await base.Negate_on_like_expression(async); + + AssertSql( +""" +SELECT `o`.`Id` +FROM `OperatorEntityString` AS `o` +WHERE `o`.`Value` NOT LIKE 'A%' OR (`o`.`Value` IS NULL) +"""); + } + + public override Task Concat_and_json_scalar(bool async) + => Assert.ThrowsAsync(() => base.Concat_and_json_scalar(async)); + + protected override void Seed(OperatorsContext ctx) + { + ctx.Set().AddRange(ExpectedData.OperatorEntitiesString); + ctx.Set().AddRange(ExpectedData.OperatorEntitiesInt); + ctx.Set().AddRange(ExpectedData.OperatorEntitiesNullableInt); + ctx.Set().AddRange(ExpectedData.OperatorEntitiesLong); + ctx.Set().AddRange(ExpectedData.OperatorEntitiesBool); + ctx.Set().AddRange(ExpectedData.OperatorEntitiesNullableBool); + // ctx.Set().AddRange(ExpectedData.OperatorEntitiesDateTimeOffset); + + ctx.SaveChanges(); + } +} diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/OptionalDependentQuerySingleStoreFixture.cs b/test/EFCore.SingleStore.FunctionalTests/Query/OptionalDependentQuerySingleStoreFixture.cs new file mode 100644 index 000000000..b33ac3f76 --- /dev/null +++ b/test/EFCore.SingleStore.FunctionalTests/Query/OptionalDependentQuerySingleStoreFixture.cs @@ -0,0 +1,11 @@ +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.TestUtilities; +using EntityFrameworkCore.SingleStore.FunctionalTests.TestUtilities; + +namespace EntityFrameworkCore.SingleStore.FunctionalTests.Query; + +public class OptionalDependentQuerySingleStoreFixture : OptionalDependentQueryFixtureBase +{ + protected override ITestStoreFactory TestStoreFactory + => SingleStoreTestStoreFactory.Instance; +} diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/OptionalDependentQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/OptionalDependentQuerySingleStoreTest.cs new file mode 100644 index 000000000..12ddb5962 --- /dev/null +++ b/test/EFCore.SingleStore.FunctionalTests/Query/OptionalDependentQuerySingleStoreTest.cs @@ -0,0 +1,142 @@ +using System; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Query; +using EntityFrameworkCore.SingleStore.Infrastructure; +using EntityFrameworkCore.SingleStore.Tests.TestUtilities.Attributes; +using Xunit; +using Xunit.Abstractions; + +namespace EntityFrameworkCore.SingleStore.FunctionalTests.Query; + +// Currently skipped by using `internal` instead of `public` as class access modifier. +[SupportedServerVersionCondition(nameof(ServerVersionSupport.Json))] +internal class OptionalDependentQuerySingleStoreTest : OptionalDependentQueryTestBase +{ + public OptionalDependentQuerySingleStoreTest(OptionalDependentQuerySingleStoreFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + // Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task Basic_projection_entity_with_all_optional(bool async) + { + await Assert.ThrowsAsync(() => base.Basic_projection_entity_with_all_optional(async)); + +// AssertSql( +// """ +// SELECT [e].[Id], [e].[Name], [e].[Json] +// FROM [EntitiesAllOptional] AS [e] +// """); + } + + public override async Task Basic_projection_entity_with_some_required(bool async) + { + await Assert.ThrowsAsync(() => base.Basic_projection_entity_with_some_required(async)); + +// AssertSql( +// """ +// SELECT [e].[Id], [e].[Name], [e].[Json] +// FROM [EntitiesSomeRequired] AS [e] +// """); + } + + public override async Task Filter_optional_dependent_with_all_optional_compared_to_null(bool async) + { + await Assert.ThrowsAsync(() => base.Filter_optional_dependent_with_all_optional_compared_to_null(async)); + +// AssertSql( +// """ +// SELECT [e].[Id], [e].[Name], [e].[Json] +// FROM [EntitiesAllOptional] AS [e] +// WHERE [e].[Json] IS NULL +// """); + } + + public override async Task Filter_optional_dependent_with_all_optional_compared_to_not_null(bool async) + { + await Assert.ThrowsAsync(() => base.Filter_optional_dependent_with_all_optional_compared_to_not_null(async)); + +// AssertSql( +// """ +// SELECT [e].[Id], [e].[Name], [e].[Json] +// FROM [EntitiesAllOptional] AS [e] +// WHERE [e].[Json] IS NOT NULL +// """); + } + + public override async Task Filter_optional_dependent_with_some_required_compared_to_null(bool async) + { + await Assert.ThrowsAsync(() => base.Filter_optional_dependent_with_some_required_compared_to_null(async)); + +// AssertSql( +// """ +// SELECT [e].[Id], [e].[Name], [e].[Json] +// FROM [EntitiesSomeRequired] AS [e] +// WHERE [e].[Json] IS NULL +// """); + } + + public override async Task Filter_optional_dependent_with_some_required_compared_to_not_null(bool async) + { + await Assert.ThrowsAsync(() => base.Filter_optional_dependent_with_some_required_compared_to_not_null(async)); + +// AssertSql( +// """ +// SELECT [e].[Id], [e].[Name], [e].[Json] +// FROM [EntitiesSomeRequired] AS [e] +// WHERE [e].[Json] IS NOT NULL +// """); + } + + public override async Task Filter_nested_optional_dependent_with_all_optional_compared_to_null(bool async) + { + await Assert.ThrowsAsync(() => base.Filter_nested_optional_dependent_with_all_optional_compared_to_null(async)); + +// AssertSql( +// """ +// SELECT [e].[Id], [e].[Name], [e].[Json] +// FROM [EntitiesAllOptional] AS [e] +// WHERE JSON_QUERY([e].[Json], '$.OpNav1') IS NULL +// """); + } + + public override async Task Filter_nested_optional_dependent_with_all_optional_compared_to_not_null(bool async) + { + await Assert.ThrowsAsync(() => base.Filter_nested_optional_dependent_with_all_optional_compared_to_not_null(async)); + +// AssertSql( +// """ +// SELECT [e].[Id], [e].[Name], [e].[Json] +// FROM [EntitiesAllOptional] AS [e] +// WHERE JSON_QUERY([e].[Json], '$.OpNav2') IS NOT NULL +// """); + } + + public override async Task Filter_nested_optional_dependent_with_some_required_compared_to_null(bool async) + { + await Assert.ThrowsAsync(() => base.Filter_nested_optional_dependent_with_some_required_compared_to_null(async)); + +// AssertSql( +// """ +// SELECT [e].[Id], [e].[Name], [e].[Json] +// FROM [EntitiesSomeRequired] AS [e] +// WHERE JSON_QUERY([e].[Json], '$.ReqNav1') IS NULL +// """); + } + + public override async Task Filter_nested_optional_dependent_with_some_required_compared_to_not_null(bool async) + { + await Assert.ThrowsAsync(() => base.Filter_nested_optional_dependent_with_some_required_compared_to_not_null(async)); + +// AssertSql( +// """ +// SELECT [e].[Id], [e].[Name], [e].[Json] +// FROM [EntitiesSomeRequired] AS [e] +// WHERE JSON_QUERY([e].[Json], '$.ReqNav2') IS NOT NULL +// """); + } + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/OwnedEntityQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/OwnedEntityQuerySingleStoreTest.cs index 1532a007f..e8bad8d63 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/OwnedEntityQuerySingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/OwnedEntityQuerySingleStoreTest.cs @@ -240,7 +240,7 @@ public override async Task Projecting_correlated_collection_property_for_owned_e Assert.True(new[] { "US", "CA" }.SequenceEqual(warehouseModel.DestinationCountryCodes)); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] [MemberData("IsAsyncData", new object[] { })] public override async Task Projecting_owned_collection_and_aggregate(bool async) { diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/PrimitiveCollectionsQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/PrimitiveCollectionsQuerySingleStoreTest.cs new file mode 100644 index 000000000..02af71bf9 --- /dev/null +++ b/test/EFCore.SingleStore.FunctionalTests/Query/PrimitiveCollectionsQuerySingleStoreTest.cs @@ -0,0 +1,1680 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.TestUtilities; +using EntityFrameworkCore.SingleStore.FunctionalTests.TestUtilities; +using EntityFrameworkCore.SingleStore.Infrastructure; +using EntityFrameworkCore.SingleStore.Tests; +using EntityFrameworkCore.SingleStore.Tests.TestUtilities.Attributes; +using Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace EntityFrameworkCore.SingleStore.FunctionalTests.Query; + +public class PrimitiveCollectionsQuerySingleStoreTest : PrimitiveCollectionsQueryRelationalTestBase< + PrimitiveCollectionsQuerySingleStoreTest.PrimitiveCollectionsQuerySingleStoreFixture> +{ + public PrimitiveCollectionsQuerySingleStoreTest(PrimitiveCollectionsQuerySingleStoreFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task Inline_collection_of_ints_Contains(bool async) + { + await base.Inline_collection_of_ints_Contains(async); + + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE `p`.`Int` IN (10, 999) +"""); + } + + public override async Task Inline_collection_of_nullable_ints_Contains(bool async) + { + await base.Inline_collection_of_nullable_ints_Contains(async); + + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE `p`.`NullableInt` IN (10, 999) +"""); + } + + public override async Task Inline_collection_of_nullable_ints_Contains_null(bool async) + { + await base.Inline_collection_of_nullable_ints_Contains_null(async); + + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE `p`.`NullableInt` IS NULL OR (`p`.`NullableInt` = 999) +"""); + } + + public override Task Inline_collection_Count_with_zero_values(bool async) + => AssertTranslationFailedWithDetails( + () => base.Inline_collection_Count_with_zero_values(async), + RelationalStrings.EmptyCollectionNotSupportedAsInlineQueryRoot); + + public override async Task Inline_collection_Count_with_one_value(bool async) + { + await base.Inline_collection_Count_with_one_value(async); + + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE ( + SELECT COUNT(*) + FROM (SELECT CAST(2 AS signed) AS `Value`) AS `v` + WHERE `v`.`Value` > `p`.`Id`) = 1 +"""); + } + + public override async Task Inline_collection_Count_with_two_values(bool async) + { + await base.Inline_collection_Count_with_two_values(async); + + if (AppConfig.ServerVersion.Supports.ValuesWithRows) + { + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE ( + SELECT COUNT(*) + FROM (SELECT CAST(2 AS signed) AS `Value` UNION ALL VALUES ROW(999)) AS `v` + WHERE `v`.`Value` > `p`.`Id`) = 1 +"""); + } + else if (AppConfig.ServerVersion.Supports.Values) + { + + } + else + { + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE ( + SELECT COUNT(*) + FROM (SELECT CAST(2 AS signed) AS `Value` UNION ALL SELECT 999) AS `v` + WHERE `v`.`Value` > `p`.`Id`) = 1 +"""); + } + } + + public override async Task Inline_collection_Count_with_three_values(bool async) + { + await base.Inline_collection_Count_with_three_values(async); + + if (AppConfig.ServerVersion.Supports.ValuesWithRows) + { + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE ( + SELECT COUNT(*) + FROM (SELECT CAST(2 AS signed) AS `Value` UNION ALL VALUES ROW(999), ROW(1000)) AS `v` + WHERE `v`.`Value` > `p`.`Id`) = 2 +"""); + } + else if (AppConfig.ServerVersion.Supports.Values) + { + } + else + { + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE ( + SELECT COUNT(*) + FROM (SELECT CAST(2 AS signed) AS `Value` UNION ALL SELECT 999 UNION ALL SELECT 1000) AS `v` + WHERE `v`.`Value` > `p`.`Id`) = 2 +"""); + } + } + + public override Task Inline_collection_Contains_with_zero_values(bool async) + => AssertTranslationFailedWithDetails( + () => base.Inline_collection_Contains_with_zero_values(async), + RelationalStrings.EmptyCollectionNotSupportedAsInlineQueryRoot); + + public override async Task Inline_collection_Contains_with_one_value(bool async) + { + await base.Inline_collection_Contains_with_one_value(async); + + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE `p`.`Id` = 2 +"""); + } + + public override async Task Inline_collection_Contains_with_two_values(bool async) + { + await base.Inline_collection_Contains_with_two_values(async); + + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE `p`.`Id` IN (2, 999) +"""); + } + + public override async Task Inline_collection_Contains_with_three_values(bool async) + { + await base.Inline_collection_Contains_with_three_values(async); + + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE `p`.`Id` IN (2, 999, 1000) +"""); + } + + public override async Task Inline_collection_Contains_with_all_parameters(bool async) + { + await base.Inline_collection_Contains_with_all_parameters(async); + + AssertSql( +""" +@__i_0='2' +@__j_1='999' +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE `p`.`Id` IN (@__i_0, @__j_1) +"""); + } + + public override async Task Inline_collection_Contains_with_constant_and_parameter(bool async) + { + await base.Inline_collection_Contains_with_constant_and_parameter(async); + + AssertSql( +""" +@__j_0='999' +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE `p`.`Id` IN (2, @__j_0) +"""); + } + + public override async Task Inline_collection_Contains_with_mixed_value_types(bool async) + { + await base.Inline_collection_Contains_with_mixed_value_types(async); + + AssertSql( +""" +@__i_0='11' +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE `p`.`Int` IN (999, @__i_0, `p`.`Id`, `p`.`Id` + `p`.`Int`) +"""); + } + + public override async Task Inline_collection_negated_Contains_as_All(bool async) + { + await base.Inline_collection_negated_Contains_as_All(async); + + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE `p`.`Id` NOT IN (2, 999) +"""); + } + + public override async Task Parameter_collection_Count(bool async) + { + await base.Parameter_collection_Count(async); + + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE ( + SELECT COUNT(*) + FROM JSON_TABLE('[2,999]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `i` + WHERE `i`.`value` > `p`.`Id`) = 1 +"""); + } + + public override async Task Parameter_collection_of_ints_Contains(bool async) + { + await base.Parameter_collection_of_ints_Contains(async); + + if (SingleStoreTestHelpers.HasPrimitiveCollectionsSupport(Fixture)) + { + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE `p`.`Int` IN ( + SELECT `i`.`value` + FROM JSON_TABLE('[10,999]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `i` +) +"""); + } + else + { + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE `p`.`Int` IN (10, 999) +"""); + } + } + + public override async Task Parameter_collection_of_nullable_ints_Contains_int(bool async) + { + await base.Parameter_collection_of_nullable_ints_Contains_int(async); + + if (SingleStoreTestHelpers.HasPrimitiveCollectionsSupport(Fixture)) + { + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE `p`.`Int` IN ( + SELECT `n`.`value` + FROM JSON_TABLE('[10,999]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `n` +) +"""); + } + else + { + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE `p`.`Int` IN (10, 999) +"""); + } + } + + public override async Task Parameter_collection_of_nullable_ints_Contains_nullable_int(bool async) + { + await base.Parameter_collection_of_nullable_ints_Contains_nullable_int(async); + + if (SingleStoreTestHelpers.HasPrimitiveCollectionsSupport(Fixture)) + { + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE EXISTS ( + SELECT 1 + FROM JSON_TABLE('[null,999]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `n` + WHERE (`n`.`value` = `p`.`NullableInt`) OR (`n`.`value` IS NULL AND (`p`.`NullableInt` IS NULL))) +"""); + } + else + { + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE `p`.`NullableInt` IS NULL OR (`p`.`NullableInt` = 999) +"""); + } + } + + public override async Task Parameter_collection_of_strings_Contains_nullable_string(bool async) + { + await base.Parameter_collection_of_strings_Contains_nullable_string(async); + + if (SingleStoreTestHelpers.HasPrimitiveCollectionsSupport(Fixture)) + { + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE EXISTS ( + SELECT 1 + FROM JSON_TABLE('["999",null]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` longtext PATH '$[0]' + )) AS `s` + WHERE (`s`.`value` = `p`.`NullableString`) OR (`s`.`value` IS NULL AND (`p`.`NullableString` IS NULL))) +"""); + } + else + { + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE `p`.`NullableString` IS NULL OR (`p`.`NullableString` = '999') +"""); + } + } + + public override async Task Parameter_collection_of_strings_Contains_non_nullable_string(bool async) + { + await base.Parameter_collection_of_strings_Contains_non_nullable_string(async); + + if (SingleStoreTestHelpers.HasPrimitiveCollectionsSupport(Fixture)) + { + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE `p`.`String` IN ( + SELECT `s`.`value` + FROM JSON_TABLE('["10","999"]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` longtext PATH '$[0]' + )) AS `s` +) +"""); + } + else + { + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE `p`.`String` IN ('10', '999') +"""); + } + } + + public override async Task Parameter_collection_of_DateTimes_Contains(bool async) + { + await base.Parameter_collection_of_DateTimes_Contains(async); + + if (SingleStoreTestHelpers.HasPrimitiveCollectionsSupport(Fixture)) + { + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE `p`.`DateTime` IN ( + SELECT `d`.`value` + FROM JSON_TABLE('["2020-01-10T12:30:00Z","9999-01-01T00:00:00Z"]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` datetime(6) PATH '$[0]' + )) AS `d` +) +"""); + } + else + { + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE `p`.`DateTime` IN (TIMESTAMP '2020-01-10 12:30:00', TIMESTAMP '9999-01-01 00:00:00') +"""); + } + } + + public override async Task Parameter_collection_of_bools_Contains(bool async) + { + await base.Parameter_collection_of_bools_Contains(async); + + if (SingleStoreTestHelpers.HasPrimitiveCollectionsSupport(Fixture)) + { + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE `p`.`Bool` IN ( + SELECT `b`.`value` + FROM JSON_TABLE('[true]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` tinyint(1) PATH '$[0]' + )) AS `b` +) +"""); + } + else + { + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE `p`.`Bool` +"""); + } + } + + public override async Task Parameter_collection_of_enums_Contains(bool async) + { + await base.Parameter_collection_of_enums_Contains(async); + + if (SingleStoreTestHelpers.HasPrimitiveCollectionsSupport(Fixture)) + { + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE `p`.`Enum` IN ( + SELECT `e`.`value` + FROM JSON_TABLE('[0,3]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `e` +) +"""); + } + else + { + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE `p`.`Enum` IN (0, 3) +"""); + } + } + + public override async Task Parameter_collection_null_Contains(bool async) + { + await base.Parameter_collection_null_Contains(async); + + if (SingleStoreTestHelpers.HasPrimitiveCollectionsSupport(Fixture)) + { + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE `p`.`Int` IN ( + SELECT `i`.`value` + FROM JSON_TABLE(NULL, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `i` +) +"""); + } + else + { + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE FALSE +"""); + } + } + + [SupportedServerVersionCondition(nameof(ServerVersionSupport.JsonTableImplementationWithoutMySqlBugs))] + public override async Task Column_collection_of_ints_Contains(bool async) + { + await base.Column_collection_of_ints_Contains(async); + + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE 10 IN ( + SELECT `i`.`value` + FROM JSON_TABLE(`p`.`Ints`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `i` +) +"""); + } + + [SupportedServerVersionCondition(nameof(ServerVersionSupport.JsonTableImplementationWithoutMySqlBugs))] + public override async Task Column_collection_of_nullable_ints_Contains(bool async) + { + await base.Column_collection_of_nullable_ints_Contains(async); + + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE 10 IN ( + SELECT `n`.`value` + FROM JSON_TABLE(`p`.`NullableInts`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `n` +) +"""); + } + + [SupportedServerVersionCondition(nameof(ServerVersionSupport.JsonTableImplementationWithoutMySqlBugs))] + public override async Task Column_collection_of_nullable_ints_Contains_null(bool async) + { + await base.Column_collection_of_nullable_ints_Contains_null(async); + + AssertSql( +""" +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE array_position(p."NullableInts", NULL) IS NOT NULL +"""); + } + + public override async Task Column_collection_of_strings_contains_null(bool async) + { + await base.Column_collection_of_strings_contains_null(async); + + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE FALSE +"""); + } + + [SupportedServerVersionCondition(nameof(ServerVersionSupport.JsonTableImplementationWithoutMySqlBugs))] + public override async Task Column_collection_of_nullable_strings_contains_null(bool async) + { + await base.Column_collection_of_nullable_strings_contains_null(async); + + AssertSql( +""" +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE array_position(p."NullableStrings", NULL) IS NOT NULL +"""); + } + + [SupportedServerVersionCondition(nameof(ServerVersionSupport.JsonTableImplementationWithoutMySqlBugs))] + public override async Task Column_collection_of_bools_Contains(bool async) + { + await base.Column_collection_of_bools_Contains(async); + + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE TRUE IN ( + SELECT `b`.`value` + FROM JSON_TABLE(`p`.`Bools`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` tinyint(1) PATH '$[0]' + )) AS `b` +) +"""); + } + + public override async Task Column_collection_Count_method(bool async) + { + await base.Column_collection_Count_method(async); + + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE ( + SELECT COUNT(*) + FROM JSON_TABLE(`p`.`Ints`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `i`) = 2 +"""); + } + + public override async Task Column_collection_Length(bool async) + { + await base.Column_collection_Length(async); + + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE ( + SELECT COUNT(*) + FROM JSON_TABLE(`p`.`Ints`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `i`) = 2 +"""); + } + + public override async Task Column_collection_index_int(bool async) + { + await base.Column_collection_index_int(async); + + if (AppConfig.ServerVersion.Supports.JsonValue) + { + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE CAST(JSON_VALUE(`p`.`Ints`, '$[1]') AS signed) = 10 +"""); + } + else + { + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE CAST(JSON_UNQUOTE(JSON_EXTRACT(`p`.`Ints`, '$[1]')) AS signed) = 10 +"""); + } + } + + public override async Task Column_collection_index_string(bool async) + { + await base.Column_collection_index_string(async); + + if (AppConfig.ServerVersion.Supports.JsonValue) + { + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE CAST(JSON_VALUE(`p`.`Strings`, '$[1]') AS char) = '10' +"""); + } + else + { + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE CAST(JSON_UNQUOTE(JSON_EXTRACT(`p`.`Strings`, '$[1]')) AS char) = '10' +"""); + } + } + + public override async Task Column_collection_index_datetime(bool async) + { + await base.Column_collection_index_datetime(async); + + if (AppConfig.ServerVersion.Supports.JsonValue) + { + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE CAST(JSON_VALUE(`p`.`DateTimes`, '$[1]') AS datetime(6)) = TIMESTAMP '2020-01-10 12:30:00' +"""); + } + else + { + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE CAST(JSON_UNQUOTE(JSON_EXTRACT(`p`.`DateTimes`, '$[1]')) AS datetime(6)) = TIMESTAMP '2020-01-10 12:30:00' +"""); + } + } + + public override async Task Column_collection_index_beyond_end(bool async) + { + await base.Column_collection_index_beyond_end(async); + + if (AppConfig.ServerVersion.Supports.JsonValue) + { + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE CAST(JSON_VALUE(`p`.`Ints`, '$[999]') AS signed) = 10 +"""); + } + else + { + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE CAST(JSON_UNQUOTE(JSON_EXTRACT(`p`.`Ints`, '$[999]')) AS signed) = 10 +"""); + } + } + + [SupportedServerVersionCondition(nameof(ServerVersionSupport.JsonValue), Skip = "TODO: Fix NULL handling of JSON_EXTRACT().")] + public override async Task Nullable_reference_column_collection_index_equals_nullable_column(bool async) + { + await base.Nullable_reference_column_collection_index_equals_nullable_column(async); + + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE (CAST(JSON_VALUE(`p`.`NullableStrings`, '$[2]') AS char) = `p`.`NullableString`) OR ((CAST(JSON_VALUE(`p`.`NullableStrings`, '$[2]') AS char)) IS NULL AND (`p`.`NullableString` IS NULL)) +"""); + } + + public override async Task Non_nullable_reference_column_collection_index_equals_nullable_column(bool async) + { + await base.Non_nullable_reference_column_collection_index_equals_nullable_column(async); + + if (AppConfig.ServerVersion.Supports.JsonValue) + { + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE (JSON_LENGTH(`p`.`Strings`) > 0) AND (CAST(JSON_VALUE(`p`.`Strings`, '$[1]') AS char) = `p`.`NullableString`) +"""); + } + else + { + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE (JSON_LENGTH(`p`.`Strings`) > 0) AND (CAST(JSON_UNQUOTE(JSON_EXTRACT(`p`.`Strings`, '$[1]')) AS char) = `p`.`NullableString`) +"""); + } + } + + [SupportedServerVersionCondition(nameof(ServerVersionSupport.OffsetReferencesOuterQuery))] + public override async Task Inline_collection_index_Column(bool async) + { + await base.Inline_collection_index_Column(async); + + AssertSql( +""" +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE ( + SELECT v."Value" + FROM (VALUES (0, 1::int), (1, 2), (2, 3)) AS v(_ord, "Value") + ORDER BY v._ord NULLS FIRST + LIMIT 1 OFFSET p."Int") = 1 +"""); + } + + public override async Task Parameter_collection_index_Column_equal_Column(bool async) + { + await base.Parameter_collection_index_Column_equal_Column(async); + + AssertSql( +""" +@__ints_0='[0,2,3]' (Size = 4000) +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE CAST(JSON_UNQUOTE(JSON_EXTRACT(@__ints_0, CONCAT('$[', CAST(`p`.`Int` AS char), ']'))) AS signed) = `p`.`Int` +"""); + } + + public override async Task Parameter_collection_index_Column_equal_constant(bool async) + { + await base.Parameter_collection_index_Column_equal_constant(async); + + AssertSql( +""" +@__ints_0='[1,2,3]' (Size = 4000) +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE CAST(JSON_UNQUOTE(JSON_EXTRACT(@__ints_0, CONCAT('$[', CAST(`p`.`Int` AS char), ']'))) AS signed) = 1 +"""); + } + + public override async Task Column_collection_ElementAt(bool async) + { + await base.Column_collection_ElementAt(async); + + if (AppConfig.ServerVersion.Supports.JsonValue) + { + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE CAST(JSON_VALUE(`p`.`Ints`, '$[1]') AS signed) = 10 +"""); + } + else + { + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE CAST(JSON_UNQUOTE(JSON_EXTRACT(`p`.`Ints`, '$[1]')) AS signed) = 10 +"""); + } + } + + public override async Task Column_collection_Skip(bool async) + { + await base.Column_collection_Skip(async); + + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE ( + SELECT COUNT(*) + FROM ( + SELECT `i`.`key` + FROM JSON_TABLE(`p`.`Ints`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `i` + ORDER BY `i`.`key` + LIMIT 18446744073709551610 OFFSET 1 + ) AS `t`) = 2 +"""); + } + + [SupportedServerVersionCondition(nameof(ServerVersionSupport.LimitWithinInAllAnySomeSubquery))] + public override async Task Column_collection_Take(bool async) + { + await base.Column_collection_Take(async); + + AssertSql( +""" +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE 11 = ANY (p."Ints"[:2]) +"""); + } + + [SupportedServerVersionCondition(nameof(ServerVersionSupport.LimitWithinInAllAnySomeSubquery))] + public override async Task Column_collection_Skip_Take(bool async) + { + await base.Column_collection_Skip_Take(async); + + AssertSql( +""" +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE 11 = ANY (p."Ints"[2:3]) +"""); + } + + public override async Task Column_collection_OrderByDescending_ElementAt(bool async) + { + await base.Column_collection_OrderByDescending_ElementAt(async); + + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE ( + SELECT `i`.`value` + FROM JSON_TABLE(`p`.`Ints`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `i` + ORDER BY `i`.`value` DESC + LIMIT 1 OFFSET 0) = 111 +"""); + } + + public override async Task Column_collection_Any(bool async) + { + await base.Column_collection_Any(async); + + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE JSON_LENGTH(`p`.`Ints`) > 0 +"""); + } + + public override async Task Column_collection_Distinct(bool async) + { + await base.Column_collection_Distinct(async); + + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE ( + SELECT COUNT(*) + FROM ( + SELECT DISTINCT `i`.`value` + FROM JSON_TABLE(`p`.`Ints`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `i` + ) AS `t`) = 3 +"""); + } + + public override async Task Column_collection_projection_from_top_level(bool async) + { + await base.Column_collection_projection_from_top_level(async); + + AssertSql( +""" +SELECT `p`.`Ints` +FROM `PrimitiveCollectionsEntity` AS `p` +ORDER BY `p`.`Id` +"""); + } + + public override async Task Column_collection_Join_parameter_collection(bool async) + { + await base.Column_collection_Join_parameter_collection(async); + + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE ( + SELECT COUNT(*) + FROM JSON_TABLE(`p`.`Ints`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `i` + INNER JOIN JSON_TABLE('[11,111]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `i0` ON `i`.`value` = `i0`.`value`) = 2 +"""); + } + + public override async Task Inline_collection_Join_ordered_column_collection(bool async) + { + await base.Inline_collection_Join_ordered_column_collection(async); + + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE ( + SELECT COUNT(*) + FROM (SELECT CAST(11 AS signed) AS `Value` UNION ALL VALUES ROW(111)) AS `v` + INNER JOIN JSON_TABLE(`p`.`Ints`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `i` ON `v`.`Value` = `i`.`value`) = 2 +"""); + } + + public override async Task Parameter_collection_Concat_column_collection(bool async) + { + await base.Parameter_collection_Concat_column_collection(async); + + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE ( + SELECT COUNT(*) + FROM ( + SELECT `i`.`value` + FROM JSON_TABLE('[11,111]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `i` + UNION ALL + SELECT `i0`.`value` + FROM JSON_TABLE(`p`.`Ints`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `i0` + ) AS `t`) = 2 +"""); + } + + public override async Task Column_collection_Union_parameter_collection(bool async) + { + await base.Column_collection_Union_parameter_collection(async); + + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE ( + SELECT COUNT(*) + FROM ( + SELECT `i`.`value` + FROM JSON_TABLE(`p`.`Ints`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `i` + UNION + SELECT `i0`.`value` + FROM JSON_TABLE('[11,111]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `i0` + ) AS `t`) = 2 +"""); + } + + public override async Task Column_collection_Intersect_inline_collection(bool async) + { + await base.Column_collection_Intersect_inline_collection(async); + + AssertSql( +""" +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE ( + SELECT count(*)::int + FROM ( + SELECT i.value + FROM unnest(p."Ints") AS i(value) + INTERSECT + VALUES (11::int), (111) + ) AS t) = 2 +"""); + } + + public override async Task Inline_collection_Except_column_collection(bool async) + { + await base.Inline_collection_Except_column_collection(async); + + AssertSql( +""" +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE ( + SELECT count(*)::int + FROM ( + SELECT v."Value" + FROM (VALUES (11::int), (111)) AS v("Value") + EXCEPT + SELECT i.value AS "Value" + FROM unnest(p."Ints") AS i(value) + ) AS t + WHERE t."Value" % 2 = 1) = 2 +"""); + } + + public override async Task Column_collection_equality_parameter_collection(bool async) + { + await base.Column_collection_equality_parameter_collection(async); + + AssertSql( +""" +@__ints_0='[1,10]' (Size = 4000) +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE `p`.`Ints` = @__ints_0 +"""); + } + + public override async Task Column_collection_equality_inline_collection(bool async) + { + await base.Column_collection_equality_inline_collection(async); + + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE `p`.`Ints` = '[1,10]' +"""); + } + + public override async Task Column_collection_equality_inline_collection_with_parameters(bool async) + { + await base.Column_collection_equality_inline_collection_with_parameters(async); + + AssertSql(); + } + + public override async Task Parameter_collection_in_subquery_Union_column_collection_as_compiled_query(bool async) + { + await base.Parameter_collection_in_subquery_Union_column_collection_as_compiled_query(async); + + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE ( + SELECT COUNT(*) + FROM ( + SELECT `t`.`value` + FROM ( + SELECT `i`.`value`, `i`.`key` + FROM JSON_TABLE('[10,111]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `i` + ORDER BY `i`.`key` + LIMIT 18446744073709551610 OFFSET 1 + ) AS `t` + UNION + SELECT `i0`.`value` + FROM JSON_TABLE(`p`.`Ints`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `i0` + ) AS `t0`) = 3 +"""); + } + + public override async Task Parameter_collection_in_subquery_Union_column_collection(bool async) + { + await base.Parameter_collection_in_subquery_Union_column_collection(async); + + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE ( + SELECT COUNT(*) + FROM ( + SELECT `s`.`value` + FROM JSON_TABLE('[111]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `s` + UNION + SELECT `i`.`value` + FROM JSON_TABLE(`p`.`Ints`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `i` + ) AS `t`) = 3 +"""); + } + + public override async Task Parameter_collection_in_subquery_Union_column_collection_nested(bool async) + { + await base.Parameter_collection_in_subquery_Union_column_collection_nested(async); + + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE ( + SELECT COUNT(*) + FROM ( + SELECT `s`.`value` + FROM JSON_TABLE('[111]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `s` + UNION + SELECT `t1`.`value` + FROM ( + SELECT `t0`.`value` + FROM ( + SELECT DISTINCT `t2`.`value` + FROM ( + SELECT `i`.`value`, `i`.`key` + FROM JSON_TABLE(`p`.`Ints`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `i` + ORDER BY `i`.`value` + LIMIT 18446744073709551610 OFFSET 1 + ) AS `t2` + ) AS `t0` + ORDER BY `t0`.`value` DESC + LIMIT 20 + ) AS `t1` + ) AS `t`) = 3 +"""); + } + + public override void Parameter_collection_in_subquery_and_Convert_as_compiled_query() + { + // base.Parameter_collection_in_subquery_and_Convert_as_compiled_query(); + // + // AssertSql(); + + // The array indexing is translated as a subquery over e.g. OPENJSON with LIMIT/OFFSET. + // Since there's a CAST over that, the type mapping inference from the other side (p.String) doesn't propagate inside to the + // subquery. In this case, the CAST operand gets the default CLR type mapping, but that's object in this case. + // We should apply the default type mapping to the parameter, but need to figure out the exact rules when to do this. + var query = EF.CompileQuery( + (PrimitiveCollectionsContext context, object[] parameters) + => context.Set().Where(p => p.String == (string)parameters[0])); + + using var context = Fixture.CreateContext(); + + var exception = Assert.Throws(() => query(context, new[] { "foo" }).ToList()); + + if (SingleStoreTestHelpers.HasPrimitiveCollectionsSupport(Fixture)) + { + Assert.Contains("in the SQL tree does not have a type mapping assigned", exception.Message); + } + else + { + Assert.Contains("Primitive collections support has not been enabled.", exception.Message); + } + } + + public override async Task Parameter_collection_in_subquery_Union_another_parameter_collection_as_compiled_query(bool async) + { + var message = (await Assert.ThrowsAsync( + () => base.Parameter_collection_in_subquery_Union_another_parameter_collection_as_compiled_query(async))).Message; + + if (SingleStoreTestHelpers.HasPrimitiveCollectionsSupport(Fixture)) + { + Assert.Equal(RelationalStrings.SetOperationsRequireAtLeastOneSideWithValidTypeMapping("Union"), message); + } + } + + public override async Task Parameter_collection_in_subquery_Count_as_compiled_query(bool async) + { + await base.Parameter_collection_in_subquery_Count_as_compiled_query(async); + + AssertSql( +""" +SELECT COUNT(*) +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE ( + SELECT COUNT(*) + FROM ( + SELECT `i`.`value`, `i`.`key`, `i`.`value` AS `value0` + FROM JSON_TABLE('[10,111]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `i` + ORDER BY `i`.`key` + LIMIT 18446744073709551610 OFFSET 1 + ) AS `t` + WHERE `t`.`value0` > `p`.`Id`) = 1 +"""); + } + + public override async Task Column_collection_in_subquery_Union_parameter_collection(bool async) + { + await base.Column_collection_in_subquery_Union_parameter_collection(async); + + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE ( + SELECT COUNT(*) + FROM ( + SELECT `t`.`value` + FROM ( + SELECT `i`.`value`, `i`.`key` + FROM JSON_TABLE(`p`.`Ints`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `i` + ORDER BY `i`.`key` + LIMIT 18446744073709551610 OFFSET 1 + ) AS `t` + UNION + SELECT `i0`.`value` + FROM JSON_TABLE('[10,111]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `i0` + ) AS `t0`) = 3 +"""); + } + + public override async Task Project_collection_of_ints_simple(bool async) + { + await base.Project_collection_of_ints_simple(async); + + AssertSql( +""" +SELECT `p`.`Ints` +FROM `PrimitiveCollectionsEntity` AS `p` +ORDER BY `p`.`Id` +"""); + } + + [SupportedServerVersionCondition(nameof(ServerVersionSupport.JsonTableImplementationStable))] + public override async Task Project_collection_of_ints_ordered(bool async) + { + await base.Project_collection_of_ints_ordered(async); + + AssertSql( +""" +SELECT `p`.`Id`, `i`.`value`, `i`.`key` +FROM `PrimitiveCollectionsEntity` AS `p` +LEFT JOIN JSON_TABLE(`p`.`Ints`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' +)) AS `i` ON TRUE +ORDER BY `p`.`Id`, `i`.`value` DESC +"""); + } + + [SupportedServerVersionCondition(nameof(ServerVersionSupport.JsonTable))] + [SupportedServerVersionCondition(nameof(ServerVersionSupport.OuterApply))] + public override async Task Project_collection_of_datetimes_filtered(bool async) + { + if (SingleStoreTestHelpers.HasPrimitiveCollectionsSupport(Fixture)) + { + await base.Project_collection_of_datetimes_filtered(async); + + AssertSql( +""" +SELECT `p`.`Id`, `t`.`value`, `t`.`key` +FROM `PrimitiveCollectionsEntity` AS `p` +LEFT JOIN LATERAL ( + SELECT `d`.`value`, `d`.`key` + FROM JSON_TABLE(`p`.`DateTimes`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` datetime(6) PATH '$[0]' + )) AS `d` + WHERE (EXTRACT(day FROM `d`.`value`) <> 1) OR EXTRACT(day FROM `d`.`value`) IS NULL +) AS `t` ON TRUE +ORDER BY `p`.`Id`, `t`.`key` +"""); + } + else + { + await Assert.ThrowsAsync(() + => base.Project_collection_of_datetimes_filtered(async)); + } + } + + public override async Task Project_collection_of_nullable_ints_with_paging(bool async) + { + await base.Project_collection_of_nullable_ints_with_paging(async); + + if (SingleStoreTestHelpers.HasPrimitiveCollectionsSupport(Fixture)) + { + AssertSql( +""" +SELECT `p`.`Id`, `t`.`value`, `t`.`key` +FROM `PrimitiveCollectionsEntity` AS `p` +LEFT JOIN LATERAL ( + SELECT `n`.`value`, `n`.`key` + FROM JSON_TABLE(`p`.`NullableInts`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `n` + ORDER BY `n`.`key` + LIMIT 20 +) AS `t` ON TRUE +ORDER BY `p`.`Id`, `t`.`key` +"""); + } + else + { + AssertSql( +""" +SELECT `p`.`NullableInts` +FROM `PrimitiveCollectionsEntity` AS `p` +ORDER BY `p`.`Id` +"""); + } + } + + [SupportedServerVersionCondition(nameof(ServerVersionSupport.JsonTable))] + [SupportedServerVersionCondition(nameof(ServerVersionSupport.OuterApply))] + public override async Task Project_collection_of_nullable_ints_with_paging2(bool async) + { + if (SingleStoreTestHelpers.HasPrimitiveCollectionsSupport(Fixture)) + { + await base.Project_collection_of_nullable_ints_with_paging2(async); + + AssertSql( +""" +SELECT `p`.`Id`, `t`.`value`, `t`.`key` +FROM `PrimitiveCollectionsEntity` AS `p` +LEFT JOIN LATERAL ( + SELECT `n`.`value`, `n`.`key` + FROM JSON_TABLE(`p`.`NullableInts`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `n` + ORDER BY `n`.`value` + LIMIT 18446744073709551610 OFFSET 1 +) AS `t` ON TRUE +ORDER BY `p`.`Id`, `t`.`value` +"""); + } + else + { + await Assert.ThrowsAsync(() + => base.Project_collection_of_nullable_ints_with_paging2(async)); + } + } + + public override async Task Project_collection_of_nullable_ints_with_paging3(bool async) + { + await base.Project_collection_of_nullable_ints_with_paging3(async); + + if (SingleStoreTestHelpers.HasPrimitiveCollectionsSupport(Fixture)) + { + AssertSql( +""" +SELECT `p`.`Id`, `t`.`value`, `t`.`key` +FROM `PrimitiveCollectionsEntity` AS `p` +LEFT JOIN LATERAL ( + SELECT `n`.`value`, `n`.`key` + FROM JSON_TABLE(`p`.`NullableInts`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `n` + ORDER BY `n`.`key` + LIMIT 18446744073709551610 OFFSET 2 +) AS `t` ON TRUE +ORDER BY `p`.`Id`, `t`.`key` +"""); + } + else + { + AssertSql( +""" +SELECT `p`.`NullableInts` +FROM `PrimitiveCollectionsEntity` AS `p` +ORDER BY `p`.`Id` +"""); + } + } + + public override async Task Project_collection_of_ints_with_distinct(bool async) + { + await base.Project_collection_of_ints_with_distinct(async); + + if (SingleStoreTestHelpers.HasPrimitiveCollectionsSupport(Fixture)) + { + AssertSql( +""" +SELECT `p`.`Id`, `t`.`value` +FROM `PrimitiveCollectionsEntity` AS `p` +LEFT JOIN LATERAL ( + SELECT DISTINCT `i`.`value` + FROM JSON_TABLE(`p`.`Ints`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `i` +) AS `t` ON TRUE +ORDER BY `p`.`Id` +"""); + } + else + { + AssertSql( +""" +SELECT `p`.`Ints` +FROM `PrimitiveCollectionsEntity` AS `p` +ORDER BY `p`.`Id` +"""); + } + } + + public override async Task Project_collection_of_nullable_ints_with_distinct(bool async) + { + await base.Project_collection_of_nullable_ints_with_distinct(async); + + AssertSql(); + } + + [SupportedServerVersionCondition(nameof(ServerVersionSupport.JsonTable))] + [SupportedServerVersionCondition(nameof(ServerVersionSupport.OuterApply))] + public override async Task Project_empty_collection_of_nullables_and_collection_only_containing_nulls(bool async) + { + if (SingleStoreTestHelpers.HasPrimitiveCollectionsSupport(Fixture)) + { + await base.Project_empty_collection_of_nullables_and_collection_only_containing_nulls(async); + + AssertSql( +""" +SELECT `p`.`Id`, `t`.`value`, `t`.`key`, `t0`.`value`, `t0`.`key` +FROM `PrimitiveCollectionsEntity` AS `p` +LEFT JOIN LATERAL ( + SELECT `n`.`value`, `n`.`key` + FROM JSON_TABLE(`p`.`NullableInts`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `n` + WHERE FALSE +) AS `t` ON TRUE +LEFT JOIN LATERAL ( + SELECT `n0`.`value`, `n0`.`key` + FROM JSON_TABLE(`p`.`NullableInts`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `n0` + WHERE `n0`.`value` IS NULL +) AS `t0` ON TRUE +ORDER BY `p`.`Id`, `t`.`key`, `t0`.`key` +"""); + } + else + { + await Assert.ThrowsAsync(() + => base.Project_empty_collection_of_nullables_and_collection_only_containing_nulls(async)); + } + } + + [SupportedServerVersionCondition(nameof(ServerVersionSupport.JsonTableImplementationStable))] + public override async Task Project_multiple_collections(bool async) + { + // Base implementation currently uses an Unspecified DateTime in the query, but we require a Utc one. + await AssertQuery( + async, + ss => ss.Set().OrderBy(x => x.Id).Select(x => new + { + Ints = x.Ints.ToList(), + OrderedInts = x.Ints.OrderByDescending(xx => xx).ToList(), + FilteredDateTimes = x.DateTimes.Where(xx => xx.Day != 1).ToList(), + FilteredDateTimes2 = x.DateTimes.Where(xx => xx > new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc)).ToList() + }), + elementAsserter: (e, a) => + { + AssertCollection(e.Ints, a.Ints, ordered: true); + AssertCollection(e.OrderedInts, a.OrderedInts, ordered: true); + AssertCollection(e.FilteredDateTimes, a.FilteredDateTimes, elementSorter: ee => ee); + AssertCollection(e.FilteredDateTimes2, a.FilteredDateTimes2, elementSorter: ee => ee); + }, + assertOrder: true); + + AssertSql( +""" +SELECT `p`.`Id`, `i`.`value`, `i`.`key`, `i0`.`value`, `i0`.`key`, `t`.`value`, `t`.`key`, `t0`.`value`, `t0`.`key` +FROM `PrimitiveCollectionsEntity` AS `p` +LEFT JOIN JSON_TABLE(`p`.`Ints`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' +)) AS `i` ON TRUE +LEFT JOIN JSON_TABLE(`p`.`Ints`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' +)) AS `i0` ON TRUE +LEFT JOIN LATERAL ( + SELECT `d`.`value`, `d`.`key` + FROM JSON_TABLE(`p`.`DateTimes`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` datetime(6) PATH '$[0]' + )) AS `d` + WHERE (EXTRACT(day FROM `d`.`value`) <> 1) OR EXTRACT(day FROM `d`.`value`) IS NULL +) AS `t` ON TRUE +LEFT JOIN LATERAL ( + SELECT `d0`.`value`, `d0`.`key` + FROM JSON_TABLE(`p`.`DateTimes`, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` datetime(6) PATH '$[0]' + )) AS `d0` + WHERE `d0`.`value` > TIMESTAMP '2000-01-01 00:00:00' +) AS `t0` ON TRUE +ORDER BY `p`.`Id`, `i`.`key`, `i0`.`value` DESC, `i0`.`key`, `t`.`key`, `t0`.`key` +"""); + } + + public override async Task Project_primitive_collections_element(bool async) + { + await base.Project_primitive_collections_element(async); + + if (AppConfig.ServerVersion.Supports.JsonValue) + { + AssertSql( +""" +SELECT CAST(JSON_VALUE(`p`.`Ints`, '$[0]') AS signed) AS `Indexer`, CAST(JSON_VALUE(`p`.`DateTimes`, '$[0]') AS datetime(6)) AS `EnumerableElementAt`, CAST(JSON_VALUE(`p`.`Strings`, '$[1]') AS char) AS `QueryableElementAt` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE `p`.`Id` < 4 +ORDER BY `p`.`Id` +"""); + } + else + { + AssertSql( +""" +SELECT CAST(JSON_UNQUOTE(JSON_EXTRACT(`p`.`Ints`, '$[0]')) AS signed) AS `Indexer`, CAST(JSON_UNQUOTE(JSON_EXTRACT(`p`.`DateTimes`, '$[0]')) AS datetime(6)) AS `EnumerableElementAt`, CAST(JSON_UNQUOTE(JSON_EXTRACT(`p`.`Strings`, '$[1]')) AS char) AS `QueryableElementAt` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE `p`.`Id` < 4 +ORDER BY `p`.`Id` +"""); + } + } + + public override async Task Inline_collection_Contains_as_Any_with_predicate(bool async) + { + await base.Inline_collection_Contains_as_Any_with_predicate(async); + + AssertSql( +""" +SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`String`, `p`.`Strings` +FROM `PrimitiveCollectionsEntity` AS `p` +WHERE `p`.`Id` IN (2, 999) +"""); + } + + public override async Task Column_collection_Concat_parameter_collection_equality_inline_collection(bool async) + { + await base.Column_collection_Concat_parameter_collection_equality_inline_collection(async); + + AssertSql(); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + private PrimitiveCollectionsContext CreateContext() + => Fixture.CreateContext(); + + public class PrimitiveCollectionsQuerySingleStoreFixture : PrimitiveCollectionsQueryFixtureBase + { + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + protected override ITestStoreFactory TestStoreFactory + => SingleStoreTestStoreFactory.Instance; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + // modelBuilder.Entity().Property(p => p.Bools).HasColumnType("json"); + } + } +} diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/QueryFilterFuncletizationSingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/QueryFilterFuncletizationSingleStoreTest.cs index 78930a954..754de107a 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/QueryFilterFuncletizationSingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/QueryFilterFuncletizationSingleStoreTest.cs @@ -30,39 +30,104 @@ public override void Using_DbSet_in_filter_works() { return; } + base.Using_DbSet_in_filter_works(); } - public override void DbContext_list_is_parameterized() + public override void Using_multiple_entities_with_filters_reuses_parameters() { - using var context = CreateContext(); - // Default value of TenantIds is null InExpression over null values throws - Assert.Throws(() => context.Set().ToList()); - - context.TenantIds = new List(); - var query = context.Set().ToList(); - Assert.Empty(query); + // We're skipping this test when we're running tests on Managed Service due to the specifics of + // how AUTO_INCREMENT works (https://docs.singlestore.com/cloud/reference/sql-reference/data-definition-language-ddl/create-table/#auto-increment-behavior) + if (AppConfig.ManagedService) + { + return; + } - context.TenantIds = new List { 1 }; - query = context.Set().ToList(); - Assert.Single(query); + base.Using_multiple_entities_with_filters_reuses_parameters(); + } - context.TenantIds = new List { 2, 3 }; - query = context.Set().ToList(); - Assert.Equal(2, query.Count); + public override void DbContext_list_is_parameterized() + { + base.DbContext_list_is_parameterized(); - AssertSql( - @"SELECT `l`.`Id`, `l`.`Tenant` + if (SingleStoreTestHelpers.HasPrimitiveCollectionsSupport(Fixture)) + { + AssertSql( +""" +SELECT `l`.`Id`, `l`.`Tenant` +FROM `ListFilter` AS `l` +WHERE `l`.`Tenant` IN ( + SELECT `e`.`value` + FROM JSON_TABLE(NULL, '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `e` +) +""", + // +""" +SELECT `l`.`Id`, `l`.`Tenant` FROM `ListFilter` AS `l` -WHERE FALSE", - // - @"SELECT `l`.`Id`, `l`.`Tenant` +WHERE `l`.`Tenant` IN ( + SELECT `e`.`value` + FROM JSON_TABLE('[]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `e` +) +""", + // +""" +SELECT `l`.`Id`, `l`.`Tenant` FROM `ListFilter` AS `l` -WHERE `l`.`Tenant` = 1", - // - @"SELECT `l`.`Id`, `l`.`Tenant` +WHERE `l`.`Tenant` IN ( + SELECT `e`.`value` + FROM JSON_TABLE('[1]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `e` +) +""", + // +""" +SELECT `l`.`Id`, `l`.`Tenant` FROM `ListFilter` AS `l` -WHERE `l`.`Tenant` IN (2, 3)"); +WHERE `l`.`Tenant` IN ( + SELECT `e`.`value` + FROM JSON_TABLE('[2,3]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `e` +) +"""); + } + else + { + AssertSql( +""" +SELECT `l`.`Id`, `l`.`Tenant` +FROM `ListFilter` AS `l` +WHERE FALSE +""", + // +""" +SELECT `l`.`Id`, `l`.`Tenant` +FROM `ListFilter` AS `l` +WHERE FALSE +""", + // +""" +SELECT `l`.`Id`, `l`.`Tenant` +FROM `ListFilter` AS `l` +WHERE `l`.`Tenant` = 1 +""", + // +""" +SELECT `l`.`Id`, `l`.`Tenant` +FROM `ListFilter` AS `l` +WHERE `l`.`Tenant` IN (2, 3) +"""); + } } private void AssertSql(params string[] expected) @@ -156,6 +221,15 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con modelBuilder.Entity() .Property(e => e.Id) .HasColumnType("bigint"); + modelBuilder.Entity() + .Property(e => e.Id) + .HasColumnType("bigint"); + modelBuilder.Entity() + .Property(e => e.Id) + .HasColumnType("bigint"); + modelBuilder.Entity() + .Property(e => e.Id) + .HasColumnType("bigint"); } } } diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/SharedTypeQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/SharedTypeQuerySingleStoreTest.cs index 29e7c9e38..eaa7d3d58 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/SharedTypeQuerySingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/SharedTypeQuerySingleStoreTest.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 System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -8,6 +9,7 @@ using Microsoft.EntityFrameworkCore.TestUtilities; using EntityFrameworkCore.SingleStore.FunctionalTests.TestUtilities; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; using Xunit; namespace EntityFrameworkCore.SingleStore.FunctionalTests.Query @@ -80,5 +82,53 @@ SELECT 1 ) AS `s` WHERE (`s`.`Value` = `v`.`Value`) OR (`s`.`Value` IS NULL AND (`v`.`Value` IS NULL)))"); } + + [ConditionalFact] + public override void Ad_hoc_query_for_shared_type_entity_type_works() + { + var contextFactory = Initialize( + seed: c => c.Seed(), onModelCreating: modelBuilder => + { + modelBuilder.SharedTypeEntity>("STET", + b => + { + // We're changing the data type of the fields from INT to BIGINT, because in SingleStore + // on a sharded (distributed) table, AUTO_INCREMENT can only be used on a BIGINT column + b.IndexerProperty("Id"); + b.IndexerProperty("Value"); + }); + }); + + using var context = contextFactory.CreateContext(); + + var result = context.Database.SqlQueryRaw( + ((RelationalTestStore)TestStore).NormalizeDelimitersInRawString(@"SELECT * FROM [ViewQuery24601]")); + + Assert.Empty(result); + } + + [ConditionalFact] + public override void Ad_hoc_query_for_default_shared_type_entity_type_throws() + { + var contextFactory = Initialize( + seed: c => c.Seed(), onModelCreating: modelBuilder => + { + modelBuilder.SharedTypeEntity>("STET", + b => + { + // We're changing the data type of the fields from INT to BIGINT, because in SingleStore + // on a sharded (distributed) table, AUTO_INCREMENT can only be used on a BIGINT column + b.IndexerProperty("Id"); + b.IndexerProperty("Value"); + }); + }); + + using var context = contextFactory.CreateContext(); + + Assert.Equal( + CoreStrings.ClashingSharedType("Dictionary"), + Assert.Throws( + () => context.Database.SqlQueryRaw>(@"SELECT * FROM X")).Message); + } } } diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/SqlExecutorSingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/SqlExecutorSingleStoreTest.cs index a951c264a..27a72c26b 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/SqlExecutorSingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/SqlExecutorSingleStoreTest.cs @@ -5,14 +5,17 @@ using Microsoft.EntityFrameworkCore.TestUtilities; using SingleStoreConnector; using Xunit; +using Xunit.Abstractions; namespace EntityFrameworkCore.SingleStore.FunctionalTests.Query { public class SqlExecutorSingleStoreTest : SqlExecutorTestBase> { - public SqlExecutorSingleStoreTest(NorthwindQuerySingleStoreFixture fixture) + public SqlExecutorSingleStoreTest(NorthwindQuerySingleStoreFixture fixture, ITestOutputHelper testOutputHelper) : base(fixture) { + Fixture.TestSqlLoggerFactory.Clear(); + // Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } protected virtual int DefaultStoredProcedureResult @@ -21,147 +24,144 @@ protected virtual int DefaultStoredProcedureResult protected virtual int DefaultSqlResult => -1; - public override void Executes_stored_procedure() + public override async Task Executes_stored_procedure(bool async) { using var context = CreateContext(); - Assert.Equal(DefaultStoredProcedureResult, context.Database.ExecuteSqlRaw(TenMostExpensiveProductsSproc)); + + Assert.Equal( + DefaultStoredProcedureResult, + async + ? await context.Database.ExecuteSqlRawAsync(TenMostExpensiveProductsSproc) + : context.Database.ExecuteSqlRaw(TenMostExpensiveProductsSproc)); + + AssertSql(TenMostExpensiveProductsSproc); } - public override void Executes_stored_procedure_with_parameter() + public override async Task Executes_stored_procedure_with_parameter(bool async) { using var context = CreateContext(); var parameter = CreateDbParameter("@CustomerID", "ALFKI"); - Assert.Equal(DefaultStoredProcedureResult, context.Database.ExecuteSqlRaw(CustomerOrderHistorySproc, parameter)); + Assert.Equal( + DefaultStoredProcedureResult, + async + ? await context.Database.ExecuteSqlRawAsync(CustomerOrderHistorySproc, parameter) + : context.Database.ExecuteSqlRaw(CustomerOrderHistorySproc, parameter)); + + AssertSql( +""" +@CustomerID='ALFKI' (Nullable = false) + +CALL `CustOrderHist`(@CustomerID) +"""); } - public override void Executes_stored_procedure_with_generated_parameter() + public override async Task Executes_stored_procedure_with_generated_parameter(bool async) { using var context = CreateContext(); - Assert.Equal(DefaultStoredProcedureResult, context.Database.ExecuteSqlRaw(CustomerOrderHistoryWithGeneratedParameterSproc, "ALFKI")); + + Assert.Equal( + DefaultStoredProcedureResult, + async + ? await context.Database.ExecuteSqlRawAsync(CustomerOrderHistoryWithGeneratedParameterSproc, "ALFKI") + : context.Database.ExecuteSqlRaw(CustomerOrderHistoryWithGeneratedParameterSproc, "ALFKI")); + + AssertSql( +""" +@p0='ALFKI' (Size = 4000) + +CALL `CustOrderHist`(@p0) +"""); } - public override void Query_with_parameters_interpolated_2() + public override async Task Query_with_parameters_interpolated_2(bool async) { var city = "London"; var contactTitle = "Sales Representative"; using var context = CreateContext(); - var actual = context.Database - .ExecuteSql( + + var actual = async + ? await context.Database.ExecuteSqlAsync( + $@"SELECT COUNT(*) FROM `Customers` WHERE `City` = {city} AND `ContactTitle` = {contactTitle}") + : context.Database.ExecuteSql( $@"SELECT COUNT(*) FROM `Customers` WHERE `City` = {city} AND `ContactTitle` = {contactTitle}"); Assert.Equal(DefaultSqlResult, actual); } - public override void Query_with_DbParameters_interpolated_2() + public override async Task Query_with_DbParameters_interpolated_2(bool async) { var city = CreateDbParameter("city", "London"); var contactTitle = CreateDbParameter("contactTitle", "Sales Representative"); using var context = CreateContext(); - var actual = context.Database - .ExecuteSql( + + var actual = async + ? await context.Database.ExecuteSqlAsync( + $@"SELECT COUNT(*) FROM `Customers` WHERE `City` = {city} AND `ContactTitle` = {contactTitle}") + : context.Database.ExecuteSql( $@"SELECT COUNT(*) FROM `Customers` WHERE `City` = {city} AND `ContactTitle` = {contactTitle}"); Assert.Equal(DefaultSqlResult, actual); } - public override async Task Executes_stored_procedure_async() - { - using var context = CreateContext(); - Assert.Equal(DefaultStoredProcedureResult, await context.Database.ExecuteSqlRawAsync(TenMostExpensiveProductsSproc)); - } - - public override async Task Executes_stored_procedure_with_parameter_async() - { - using var context = CreateContext(); - var parameter = CreateDbParameter("@CustomerID", "ALFKI"); - - Assert.Equal(DefaultStoredProcedureResult, await context.Database.ExecuteSqlRawAsync(CustomerOrderHistorySproc, parameter)); - } - - public override async Task Executes_stored_procedure_with_generated_parameter_async() - { - using var context = CreateContext(); - Assert.Equal(DefaultStoredProcedureResult, await context.Database.ExecuteSqlRawAsync(CustomerOrderHistoryWithGeneratedParameterSproc, "ALFKI")); - } - - public override async Task Query_with_parameters_interpolated_async_2() + public override async Task Query_with_parameters(bool async) { var city = "London"; var contactTitle = "Sales Representative"; using var context = CreateContext(); - var actual = await context.Database - .ExecuteSqlAsync( - $@"SELECT COUNT(*) FROM `Customers` WHERE `City` = {city} AND `ContactTitle` = {contactTitle}"); - Assert.Equal(DefaultSqlResult, actual); - } + var actual = async + ? await context.Database.ExecuteSqlRawAsync( + @"SELECT COUNT(*) FROM `Customers` WHERE `City` = {0} AND `ContactTitle` = {1}", city, contactTitle) + : context.Database.ExecuteSqlRaw( + @"SELECT COUNT(*) FROM `Customers` WHERE `City` = {0} AND `ContactTitle` = {1}", city, contactTitle); - public override void Query_with_parameters() - { - var city = "London"; - var contactTitle = "Sales Representative"; - - using (var context = CreateContext()) - { - var actual = context.Database - .ExecuteSqlRaw( - @"SELECT COUNT(*) FROM `Customers` WHERE `City` = {0} AND `ContactTitle` = {1}", city, contactTitle); - - Assert.Equal(DefaultSqlResult, actual); - } + Assert.Equal(DefaultSqlResult, actual); } - [Fact] - public override void Query_with_dbParameter_with_name() + public override async Task Query_with_dbParameter_with_name(bool async) { var city = CreateDbParameter("@city", "London"); - using (var context = CreateContext()) - { - var actual = context.Database - .ExecuteSqlRaw( - @"SELECT COUNT(*) FROM `Customers` WHERE `City` = @city", city); + using var context = CreateContext(); - Assert.Equal(DefaultSqlResult, actual); - } + var actual = async + ? await context.Database.ExecuteSqlRawAsync(@"SELECT COUNT(*) FROM `Customers` WHERE `City` = @city", city) + : context.Database.ExecuteSqlRaw(@"SELECT COUNT(*) FROM `Customers` WHERE `City` = @city", city); + + Assert.Equal(DefaultSqlResult, actual); } - [Fact] - public override void Query_with_positional_dbParameter_with_name() + public override async Task Query_with_positional_dbParameter_with_name(bool async) { var city = CreateDbParameter("@city", "London"); - using (var context = CreateContext()) - { - var actual = context.Database - .ExecuteSqlRaw( - @"SELECT COUNT(*) FROM `Customers` WHERE `City` = {0}", city); + using var context = CreateContext(); + + var actual = async + ? await context.Database.ExecuteSqlRawAsync(@"SELECT COUNT(*) FROM `Customers` WHERE `City` = {0}", city) + : context.Database.ExecuteSqlRaw(@"SELECT COUNT(*) FROM `Customers` WHERE `City` = {0}", city); - Assert.Equal(DefaultSqlResult, actual); - } + Assert.Equal(DefaultSqlResult, actual); } - [Fact] - public override void Query_with_positional_dbParameter_without_name() + public override async Task Query_with_positional_dbParameter_without_name(bool async) { var city = CreateDbParameter(name: null, value: "London"); - using (var context = CreateContext()) - { - var actual = context.Database - .ExecuteSqlRaw( - @"SELECT COUNT(*) FROM `Customers` WHERE `City` = {0}", city); + using var context = CreateContext(); + + var actual = async + ? await context.Database.ExecuteSqlRawAsync(@"SELECT COUNT(*) FROM `Customers` WHERE `City` = {0}", city) + : context.Database.ExecuteSqlRaw(@"SELECT COUNT(*) FROM `Customers` WHERE `City` = {0}", city); - Assert.Equal(DefaultSqlResult, actual); - } + Assert.Equal(DefaultSqlResult, actual); } - [Fact] - public override void Query_with_dbParameters_mixed() + public override async Task Query_with_dbParameters_mixed(bool async) { var city = "London"; var contactTitle = "Sales Representative"; @@ -169,78 +169,54 @@ public override void Query_with_dbParameters_mixed() var cityParameter = CreateDbParameter("@city", city); var contactTitleParameter = CreateDbParameter("@contactTitle", contactTitle); - using (var context = CreateContext()) - { - var actual = context.Database - .ExecuteSqlRaw( - @"SELECT COUNT(*) FROM `Customers` WHERE `City` = {0} AND `ContactTitle` = @contactTitle", city, contactTitleParameter); - - Assert.Equal(DefaultSqlResult, actual); - - actual = context.Database - .ExecuteSqlRaw( - @"SELECT COUNT(*) FROM `Customers` WHERE `City` = @city AND `ContactTitle` = {1}", cityParameter, contactTitle); + using var context = CreateContext(); - Assert.Equal(DefaultSqlResult, actual); - } - } + var actual = async + ? await context.Database.ExecuteSqlRawAsync( + @"SELECT COUNT(*) FROM `Customers` WHERE `City` = {0} AND `ContactTitle` = @contactTitle", city, + contactTitleParameter) + : context.Database.ExecuteSqlRaw( + @"SELECT COUNT(*) FROM `Customers` WHERE `City` = {0} AND `ContactTitle` = @contactTitle", city, + contactTitleParameter); - [Fact] - public override void Query_with_parameters_interpolated() - { - var city = "London"; - var contactTitle = "Sales Representative"; + Assert.Equal(DefaultSqlResult, actual); - using (var context = CreateContext()) - { - var actual = context.Database - .ExecuteSqlInterpolated( - $@"SELECT COUNT(*) FROM `Customers` WHERE `City` = {city} AND `ContactTitle` = {contactTitle}"); + actual = async + ? await context.Database.ExecuteSqlRawAsync( + @"SELECT COUNT(*) FROM `Customers` WHERE `City` = @city AND `ContactTitle` = {1}", cityParameter, contactTitle) + : context.Database.ExecuteSqlRaw( + @"SELECT COUNT(*) FROM `Customers` WHERE `City` = @city AND `ContactTitle` = {1}", cityParameter, contactTitle); - Assert.Equal(DefaultSqlResult, actual); - } + Assert.Equal(DefaultSqlResult, actual); } - [ConditionalFact] - public override async Task Query_with_parameters_async() + public override async Task Query_with_parameters_interpolated(bool async) { var city = "London"; var contactTitle = "Sales Representative"; - using (var context = CreateContext()) - { - var actual = await context.Database - .ExecuteSqlRawAsync( - @"SELECT COUNT(*) FROM `Customers` WHERE `City` = {0} AND `ContactTitle` = {1}", city, contactTitle); - - Assert.Equal(DefaultSqlResult, actual); - } - } - - [Fact] - public override async Task Query_with_parameters_interpolated_async() - { - var city = "London"; - var contactTitle = "Sales Representative"; + using var context = CreateContext(); - using (var context = CreateContext()) - { - var actual = await context.Database - .ExecuteSqlInterpolatedAsync( - $@"SELECT COUNT(*) FROM `Customers` WHERE `City` = {city} AND `ContactTitle` = {contactTitle}"); + var actual = async + ? await context.Database.ExecuteSqlInterpolatedAsync( + $@"SELECT COUNT(*) FROM `Customers` WHERE `City` = {city} AND `ContactTitle` = {contactTitle}") + : context.Database.ExecuteSqlInterpolated( + $@"SELECT COUNT(*) FROM `Customers` WHERE `City` = {city} AND `ContactTitle` = {contactTitle}"); - Assert.Equal(DefaultSqlResult, actual); - } + Assert.Equal(DefaultSqlResult, actual); } - public override void Query_with_DbParameters_interpolated() + public override async Task Query_with_DbParameters_interpolated(bool async) { var city = CreateDbParameter("city", "London"); var contactTitle = CreateDbParameter("contactTitle", "Sales Representative"); using var context = CreateContext(); - var actual = context.Database - .ExecuteSqlInterpolated( + + var actual = async + ? await context.Database.ExecuteSqlInterpolatedAsync( + $@"SELECT COUNT(*) FROM `Customers` WHERE `City` = {city} AND `ContactTitle` = {contactTitle}") + : context.Database.ExecuteSqlInterpolated( $@"SELECT COUNT(*) FROM `Customers` WHERE `City` = {city} AND `ContactTitle` = {contactTitle}"); Assert.Equal(DefaultSqlResult, actual); @@ -258,5 +234,8 @@ protected override DbParameter CreateDbParameter(string name, object value) protected override string CustomerOrderHistorySproc => @"CALL `CustOrderHist`(@CustomerID)"; protected override string CustomerOrderHistoryWithGeneratedParameterSproc => @"CALL `CustOrderHist`({0})"; + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } } diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/SqlQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/SqlQuerySingleStoreTest.cs new file mode 100644 index 000000000..47ede1345 --- /dev/null +++ b/test/EFCore.SingleStore.FunctionalTests/Query/SqlQuerySingleStoreTest.cs @@ -0,0 +1,691 @@ +using System.Data.Common; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.TestUtilities; +using SingleStoreConnector; +using EntityFrameworkCore.SingleStore.Infrastructure; +using EntityFrameworkCore.SingleStore.Tests.TestUtilities.Attributes; +using Xunit.Abstractions; + +namespace EntityFrameworkCore.SingleStore.FunctionalTests.Query; + +public class SqlQuerySingleStoreTest : SqlQueryTestBase> +{ + public SqlQuerySingleStoreTest(NorthwindQuerySingleStoreFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task SqlQueryRaw_queryable_simple(bool async) + { + await base.SqlQueryRaw_queryable_simple(async); + + AssertSql( +""" +SELECT * FROM `Customers` WHERE `ContactName` LIKE '%z%' +"""); + } + + public override async Task SqlQueryRaw_queryable_simple_columns_out_of_order(bool async) + { + await base.SqlQueryRaw_queryable_simple_columns_out_of_order(async); + + AssertSql( +""" +SELECT `Region`, `PostalCode`, `Phone`, `Fax`, `CustomerID`, `Country`, `ContactTitle`, `ContactName`, `CompanyName`, `City`, `Address` FROM `Customers` +"""); + } + + public override async Task SqlQueryRaw_queryable_simple_columns_out_of_order_and_extra_columns(bool async) + { + await base.SqlQueryRaw_queryable_simple_columns_out_of_order_and_extra_columns(async); + + AssertSql( +""" +SELECT `Region`, `PostalCode`, `PostalCode` AS `Foo`, `Phone`, `Fax`, `CustomerID`, `Country`, `ContactTitle`, `ContactName`, `CompanyName`, `City`, `Address` FROM `Customers` +"""); + } + + public override async Task SqlQueryRaw_queryable_composed(bool async) + { + await base.SqlQueryRaw_queryable_composed(async); + + AssertSql( +""" +SELECT `m`.`Address`, `m`.`City`, `m`.`CompanyName`, `m`.`ContactName`, `m`.`ContactTitle`, `m`.`Country`, `m`.`CustomerID`, `m`.`Fax`, `m`.`Phone`, `m`.`Region`, `m`.`PostalCode` +FROM ( + SELECT * FROM `Customers` +) AS `m` +WHERE `m`.`ContactName` LIKE '%z%' +"""); + } + + public override async Task SqlQueryRaw_queryable_composed_after_removing_whitespaces(bool async) + { + await base.SqlQueryRaw_queryable_composed_after_removing_whitespaces(async); + + AssertSql( +"SELECT `m`.`Address`, `m`.`City`, `m`.`CompanyName`, `m`.`ContactName`, `m`.`ContactTitle`, `m`.`Country`, `m`.`CustomerID`, `m`.`Fax`, `m`.`Phone`, `m`.`Region`, `m`.`PostalCode`\r\nFROM (\r\n\r\n \r\n\r\n\r\n SELECT\r\n * FROM `Customers`\r\n) AS `m`\r\nWHERE `m`.`ContactName` LIKE '%z%'"); + } + + public override async Task SqlQueryRaw_queryable_composed_compiled(bool async) + { + await base.SqlQueryRaw_queryable_composed_compiled(async); + + AssertSql( +""" +SELECT `m`.`Address`, `m`.`City`, `m`.`CompanyName`, `m`.`ContactName`, `m`.`ContactTitle`, `m`.`Country`, `m`.`CustomerID`, `m`.`Fax`, `m`.`Phone`, `m`.`Region`, `m`.`PostalCode` +FROM ( + SELECT * FROM `Customers` +) AS `m` +WHERE `m`.`ContactName` LIKE '%z%' +"""); + } + + public override async Task SqlQueryRaw_queryable_composed_compiled_with_DbParameter(bool async) + { + await base.SqlQueryRaw_queryable_composed_compiled_with_DbParameter(async); + + AssertSql( +""" +customer='CONSH' (Nullable = false) +SELECT `m`.`Address`, `m`.`City`, `m`.`CompanyName`, `m`.`ContactName`, `m`.`ContactTitle`, `m`.`Country`, `m`.`CustomerID`, `m`.`Fax`, `m`.`Phone`, `m`.`Region`, `m`.`PostalCode` +FROM ( + SELECT * FROM `Customers` WHERE `CustomerID` = @customer +) AS `m` +WHERE `m`.`ContactName` LIKE '%z%' +"""); + } + + public override async Task SqlQueryRaw_queryable_composed_compiled_with_nameless_DbParameter(bool async) + { + await base.SqlQueryRaw_queryable_composed_compiled_with_nameless_DbParameter(async); + + AssertSql( +""" +p0='CONSH' (Nullable = false) +SELECT `m`.`Address`, `m`.`City`, `m`.`CompanyName`, `m`.`ContactName`, `m`.`ContactTitle`, `m`.`Country`, `m`.`CustomerID`, `m`.`Fax`, `m`.`Phone`, `m`.`Region`, `m`.`PostalCode` +FROM ( + SELECT * FROM `Customers` WHERE `CustomerID` = @p0 +) AS `m` +WHERE `m`.`ContactName` LIKE '%z%' +"""); + } + + public override async Task SqlQueryRaw_queryable_composed_compiled_with_parameter(bool async) + { + await base.SqlQueryRaw_queryable_composed_compiled_with_parameter(async); + + AssertSql( +""" +SELECT `m`.`Address`, `m`.`City`, `m`.`CompanyName`, `m`.`ContactName`, `m`.`ContactTitle`, `m`.`Country`, `m`.`CustomerID`, `m`.`Fax`, `m`.`Phone`, `m`.`Region`, `m`.`PostalCode` +FROM ( + SELECT * FROM `Customers` WHERE `CustomerID` = 'CONSH' +) AS `m` +WHERE `m`.`ContactName` LIKE '%z%' +"""); + } + + public override async Task SqlQueryRaw_composed_contains(bool async) + { + await base.SqlQueryRaw_composed_contains(async); + + AssertSql( +""" +SELECT `m`.`Address`, `m`.`City`, `m`.`CompanyName`, `m`.`ContactName`, `m`.`ContactTitle`, `m`.`Country`, `m`.`CustomerID`, `m`.`Fax`, `m`.`Phone`, `m`.`Region`, `m`.`PostalCode` +FROM ( + SELECT * FROM `Customers` +) AS `m` +WHERE `m`.`CustomerID` IN ( + SELECT `m0`.`CustomerID` + FROM ( + SELECT * FROM `Orders` + ) AS `m0` +) +"""); + } + + public override async Task SqlQueryRaw_queryable_multiple_composed(bool async) + { + await base.SqlQueryRaw_queryable_multiple_composed(async); + + AssertSql( +""" +SELECT `m`.`Address`, `m`.`City`, `m`.`CompanyName`, `m`.`ContactName`, `m`.`ContactTitle`, `m`.`Country`, `m`.`CustomerID`, `m`.`Fax`, `m`.`Phone`, `m`.`Region`, `m`.`PostalCode`, `m0`.`CustomerID`, `m0`.`EmployeeID`, `m0`.`Freight`, `m0`.`OrderDate`, `m0`.`OrderID`, `m0`.`RequiredDate`, `m0`.`ShipAddress`, `m0`.`ShipCity`, `m0`.`ShipCountry`, `m0`.`ShipName`, `m0`.`ShipPostalCode`, `m0`.`ShipRegion`, `m0`.`ShipVia`, `m0`.`ShippedDate` +FROM ( + SELECT * FROM `Customers` +) AS `m` +CROSS JOIN ( + SELECT * FROM `Orders` +) AS `m0` +WHERE `m`.`CustomerID` = `m0`.`CustomerID` +"""); + } + + public override Task SqlQueryRaw_queryable_multiple_composed_with_closure_parameters(bool async) + { + // Base implementation sends DateTime with Kind=Unspecified in a SQL query, but MySql rejects it because timestamptz + return Task.CompletedTask; + } + + public override Task SqlQueryRaw_queryable_multiple_composed_with_parameters_and_closure_parameters(bool async) + { + // Base implementation sends DateTime with Kind=Unspecified in a SQL query, but MySql rejects it because timestamptz + return Task.CompletedTask; + } + + public override async Task SqlQueryRaw_queryable_multiple_line_query(bool async) + { + await base.SqlQueryRaw_queryable_multiple_line_query(async); + + AssertSql( +""" +SELECT * +FROM `Customers` +WHERE `City` = 'London' +"""); + } + + public override async Task SqlQueryRaw_queryable_composed_multiple_line_query(bool async) + { + await base.SqlQueryRaw_queryable_composed_multiple_line_query(async); + + AssertSql( +""" +SELECT `m`.`Address`, `m`.`City`, `m`.`CompanyName`, `m`.`ContactName`, `m`.`ContactTitle`, `m`.`Country`, `m`.`CustomerID`, `m`.`Fax`, `m`.`Phone`, `m`.`Region`, `m`.`PostalCode` +FROM ( + SELECT * + FROM `Customers` +) AS `m` +WHERE `m`.`City` = 'London' +"""); + } + + public override async Task SqlQueryRaw_queryable_with_parameters(bool async) + { + await base.SqlQueryRaw_queryable_with_parameters(async); + + AssertSql( +""" +p0='London' (Size = 4000) +p1='Sales Representative' (Size = 4000) +SELECT * FROM `Customers` WHERE `City` = @p0 AND `ContactTitle` = @p1 +"""); + } + + public override async Task SqlQueryRaw_queryable_with_parameters_inline(bool async) + { + await base.SqlQueryRaw_queryable_with_parameters_inline(async); + + AssertSql( +""" +p0='London' (Size = 4000) +p1='Sales Representative' (Size = 4000) +SELECT * FROM `Customers` WHERE `City` = @p0 AND `ContactTitle` = @p1 +"""); + } + + public override async Task SqlQuery_queryable_with_parameters_interpolated(bool async) + { + await base.SqlQuery_queryable_with_parameters_interpolated(async); + + AssertSql( +""" +p0='London' (Size = 4000) +p1='Sales Representative' (Size = 4000) +SELECT * FROM `Customers` WHERE `City` = @p0 AND `ContactTitle` = @p1 +"""); + } + + public override async Task SqlQuery_queryable_with_parameters_inline_interpolated(bool async) + { + await base.SqlQuery_queryable_with_parameters_inline_interpolated(async); + + AssertSql( +""" +p0='London' (Size = 4000) +p1='Sales Representative' (Size = 4000) +SELECT * FROM `Customers` WHERE `City` = @p0 AND `ContactTitle` = @p1 +"""); + } + + public override Task SqlQuery_queryable_multiple_composed_with_parameters_and_closure_parameters_interpolated(bool async) + { + // Base implementation sends DateTime with Kind=Unspecified in a SQL query, but MySql rejects it because timestamptz + return Task.CompletedTask; + } + + public override async Task SqlQueryRaw_queryable_with_null_parameter(bool async) + { + await base.SqlQueryRaw_queryable_with_null_parameter(async); + + AssertSql( +""" +p0=NULL (Nullable = false) +SELECT * FROM `Employees` WHERE `ReportsTo` = @p0 OR (`ReportsTo` IS NULL AND @p0 IS NULL) +"""); + } + + public override async Task SqlQueryRaw_queryable_with_parameters_and_closure(bool async) + { + var queryString = await base.SqlQueryRaw_queryable_with_parameters_and_closure(async); + + AssertSql( +""" +p0='London' (Size = 4000) +@__contactTitle_1='Sales Representative' (Size = 30) +SELECT `m`.`Address`, `m`.`City`, `m`.`CompanyName`, `m`.`ContactName`, `m`.`ContactTitle`, `m`.`Country`, `m`.`CustomerID`, `m`.`Fax`, `m`.`Phone`, `m`.`Region`, `m`.`PostalCode` +FROM ( + SELECT * FROM `Customers` WHERE `City` = @p0 +) AS `m` +WHERE `m`.`ContactTitle` = @__contactTitle_1 +"""); + + return null; + } + + public override async Task SqlQueryRaw_queryable_simple_cache_key_includes_query_string(bool async) + { + await base.SqlQueryRaw_queryable_simple_cache_key_includes_query_string(async); + + AssertSql( +""" +SELECT * FROM `Customers` WHERE `City` = 'London' +""", + // + """ +SELECT * FROM `Customers` WHERE `City` = 'Seattle' +"""); + } + + public override async Task SqlQueryRaw_queryable_with_parameters_cache_key_includes_parameters(bool async) + { + await base.SqlQueryRaw_queryable_with_parameters_cache_key_includes_parameters(async); + + AssertSql( +""" +p0='London' (Size = 4000) +p1='Sales Representative' (Size = 4000) +SELECT * FROM `Customers` WHERE `City` = @p0 AND `ContactTitle` = @p1 +""", + // + """ +p0='Madrid' (Size = 4000) +p1='Accounting Manager' (Size = 4000) +SELECT * FROM `Customers` WHERE `City` = @p0 AND `ContactTitle` = @p1 +"""); + } + + public override async Task SqlQueryRaw_queryable_simple_as_no_tracking_not_composed(bool async) + { + await base.SqlQueryRaw_queryable_simple_as_no_tracking_not_composed(async); + + AssertSql( +""" +SELECT * FROM `Customers` +"""); + } + + public override async Task SqlQueryRaw_queryable_simple_projection_composed(bool async) + { + await base.SqlQueryRaw_queryable_simple_projection_composed(async); + + AssertSql( +""" +SELECT `m`.`ProductName` +FROM ( + SELECT * + FROM `Products` + WHERE `Discontinued` <> TRUE + AND ((`UnitsInStock` + `UnitsOnOrder`) < `ReorderLevel`) +) AS `m` +"""); + } + + public override async Task SqlQueryRaw_annotations_do_not_affect_successive_calls(bool async) + { + await base.SqlQueryRaw_annotations_do_not_affect_successive_calls(async); + + AssertSql( +""" +SELECT * FROM `Customers` WHERE `ContactName` LIKE '%z%' +""", + // + """ +SELECT * FROM `Customers` +"""); + } + + public override async Task SqlQueryRaw_composed_with_predicate(bool async) + { + await base.SqlQueryRaw_composed_with_predicate(async); + + AssertSql( +""" +SELECT `m`.`Address`, `m`.`City`, `m`.`CompanyName`, `m`.`ContactName`, `m`.`ContactTitle`, `m`.`Country`, `m`.`CustomerID`, `m`.`Fax`, `m`.`Phone`, `m`.`Region`, `m`.`PostalCode` +FROM ( + SELECT * FROM `Customers` +) AS `m` +WHERE SUBSTRING(`m`.`ContactName`, 0 + 1, 1) = SUBSTRING(`m`.`CompanyName`, 0 + 1, 1) +"""); + } + + public override async Task SqlQueryRaw_composed_with_empty_predicate(bool async) + { + await base.SqlQueryRaw_composed_with_empty_predicate(async); + + AssertSql( +""" +SELECT `m`.`Address`, `m`.`City`, `m`.`CompanyName`, `m`.`ContactName`, `m`.`ContactTitle`, `m`.`Country`, `m`.`CustomerID`, `m`.`Fax`, `m`.`Phone`, `m`.`Region`, `m`.`PostalCode` +FROM ( + SELECT * FROM `Customers` +) AS `m` +WHERE `m`.`ContactName` = `m`.`CompanyName` +"""); + } + + public override async Task SqlQueryRaw_with_dbParameter(bool async) + { + await base.SqlQueryRaw_with_dbParameter(async); + + AssertSql( +""" +@city='London' (Nullable = false) +SELECT * FROM `Customers` WHERE `City` = @city +"""); + } + + public override async Task SqlQueryRaw_with_dbParameter_without_name_prefix(bool async) + { + await base.SqlQueryRaw_with_dbParameter_without_name_prefix(async); + AssertSql( +""" +city='London' (Nullable = false) +SELECT * FROM `Customers` WHERE `City` = @city +"""); + } + + public override async Task SqlQueryRaw_with_dbParameter_mixed(bool async) + { + await base.SqlQueryRaw_with_dbParameter_mixed(async); + + AssertSql( +""" +p0='London' (Size = 4000) +@title='Sales Representative' (Nullable = false) +SELECT * FROM `Customers` WHERE `City` = @p0 AND `ContactTitle` = @title +""", + // + """ +@city='London' (Nullable = false) +p1='Sales Representative' (Size = 4000) +SELECT * FROM `Customers` WHERE `City` = @city AND `ContactTitle` = @p1 +"""); + } + + public override async Task SqlQueryRaw_with_db_parameters_called_multiple_times(bool async) + { + await base.SqlQueryRaw_with_db_parameters_called_multiple_times(async); + + AssertSql( +""" +@id='ALFKI' (Nullable = false) +SELECT * FROM `Customers` WHERE `CustomerID` = @id +""", + // + """ +@id='ALFKI' (Nullable = false) +SELECT * FROM `Customers` WHERE `CustomerID` = @id +"""); + } + + public override async Task SqlQuery_with_inlined_db_parameter(bool async) + { + await base.SqlQuery_with_inlined_db_parameter(async); + + AssertSql( +""" +@somename='ALFKI' (Nullable = false) +SELECT * FROM `Customers` WHERE `CustomerID` = @somename +"""); + } + + public override async Task SqlQuery_with_inlined_db_parameter_without_name_prefix(bool async) + { + await base.SqlQuery_with_inlined_db_parameter_without_name_prefix(async); + + AssertSql( +""" +somename='ALFKI' (Nullable = false) +SELECT * FROM `Customers` WHERE `CustomerID` = @somename +"""); + } + + public override async Task SqlQuery_parameterization_issue_12213(bool async) + { + await base.SqlQuery_parameterization_issue_12213(async); + + AssertSql( +""" +p0='10300' +SELECT `m`.`OrderID` +FROM ( + SELECT * FROM `Orders` WHERE `OrderID` >= @p0 +) AS `m` +""", + // + """ +@__max_1='10400' +p0='10300' +SELECT `m`.`OrderID` +FROM ( + SELECT * FROM `Orders` +) AS `m` +WHERE (`m`.`OrderID` <= @__max_1) AND `m`.`OrderID` IN ( + SELECT `m0`.`OrderID` + FROM ( + SELECT * FROM `Orders` WHERE `OrderID` >= @p0 + ) AS `m0` +) +""", + // + """ +@__max_1='10400' +p0='10300' +SELECT `m`.`OrderID` +FROM ( + SELECT * FROM `Orders` +) AS `m` +WHERE (`m`.`OrderID` <= @__max_1) AND `m`.`OrderID` IN ( + SELECT `m0`.`OrderID` + FROM ( + SELECT * FROM `Orders` WHERE `OrderID` >= @p0 + ) AS `m0` +) +"""); + } + + public override async Task SqlQueryRaw_does_not_parameterize_interpolated_string(bool async) + { + await base.SqlQueryRaw_does_not_parameterize_interpolated_string(async); + + AssertSql( +""" +p0='10250' +SELECT * FROM `Orders` WHERE `OrderID` < @p0 +"""); + } + + public override async Task SqlQueryRaw_with_set_operation(bool async) + { + await base.SqlQueryRaw_with_set_operation(async); + + AssertSql( +""" +SELECT `m`.`Address`, `m`.`City`, `m`.`CompanyName`, `m`.`ContactName`, `m`.`ContactTitle`, `m`.`Country`, `m`.`CustomerID`, `m`.`Fax`, `m`.`Phone`, `m`.`Region`, `m`.`PostalCode` +FROM ( + SELECT * FROM `Customers` WHERE `City` = 'London' +) AS `m` +UNION ALL +SELECT `m0`.`Address`, `m0`.`City`, `m0`.`CompanyName`, `m0`.`ContactName`, `m0`.`ContactTitle`, `m0`.`Country`, `m0`.`CustomerID`, `m0`.`Fax`, `m0`.`Phone`, `m0`.`Region`, `m0`.`PostalCode` +FROM ( + SELECT * FROM `Customers` WHERE `City` = 'Berlin' +) AS `m0` +"""); + } + + public override async Task Line_endings_after_Select(bool async) + { + await base.Line_endings_after_Select(async); + + AssertSql( +""" +SELECT `m`.`Address`, `m`.`City`, `m`.`CompanyName`, `m`.`ContactName`, `m`.`ContactTitle`, `m`.`Country`, `m`.`CustomerID`, `m`.`Fax`, `m`.`Phone`, `m`.`Region`, `m`.`PostalCode` +FROM ( + SELECT + * FROM `Customers` +) AS `m` +WHERE `m`.`City` = 'Seattle' +"""); + } + + public override async Task SqlQueryRaw_in_subquery_with_dbParameter(bool async) + { + await base.SqlQueryRaw_in_subquery_with_dbParameter(async); + + AssertSql( +""" +@city='London' (Nullable = false) +SELECT `m`.`CustomerID`, `m`.`EmployeeID`, `m`.`Freight`, `m`.`OrderDate`, `m`.`OrderID`, `m`.`RequiredDate`, `m`.`ShipAddress`, `m`.`ShipCity`, `m`.`ShipCountry`, `m`.`ShipName`, `m`.`ShipPostalCode`, `m`.`ShipRegion`, `m`.`ShipVia`, `m`.`ShippedDate` +FROM ( + SELECT * FROM `Orders` +) AS `m` +WHERE `m`.`CustomerID` IN ( + SELECT `m0`.`CustomerID` + FROM ( + SELECT * FROM `Customers` WHERE `City` = @city + ) AS `m0` +) +"""); + } + + public override async Task SqlQueryRaw_in_subquery_with_positional_dbParameter_without_name(bool async) + { + await base.SqlQueryRaw_in_subquery_with_positional_dbParameter_without_name(async); + + AssertSql( +""" +p0='London' (Nullable = false) +SELECT `m`.`CustomerID`, `m`.`EmployeeID`, `m`.`Freight`, `m`.`OrderDate`, `m`.`OrderID`, `m`.`RequiredDate`, `m`.`ShipAddress`, `m`.`ShipCity`, `m`.`ShipCountry`, `m`.`ShipName`, `m`.`ShipPostalCode`, `m`.`ShipRegion`, `m`.`ShipVia`, `m`.`ShippedDate` +FROM ( + SELECT * FROM `Orders` +) AS `m` +WHERE `m`.`CustomerID` IN ( + SELECT `m0`.`CustomerID` + FROM ( + SELECT * FROM `Customers` WHERE `City` = @p0 + ) AS `m0` +) +"""); + } + + public override async Task SqlQueryRaw_in_subquery_with_positional_dbParameter_with_name(bool async) + { + await base.SqlQueryRaw_in_subquery_with_positional_dbParameter_with_name(async); + + AssertSql( +""" +@city='London' (Nullable = false) +SELECT `m`.`CustomerID`, `m`.`EmployeeID`, `m`.`Freight`, `m`.`OrderDate`, `m`.`OrderID`, `m`.`RequiredDate`, `m`.`ShipAddress`, `m`.`ShipCity`, `m`.`ShipCountry`, `m`.`ShipName`, `m`.`ShipPostalCode`, `m`.`ShipRegion`, `m`.`ShipVia`, `m`.`ShippedDate` +FROM ( + SELECT * FROM `Orders` +) AS `m` +WHERE `m`.`CustomerID` IN ( + SELECT `m0`.`CustomerID` + FROM ( + SELECT * FROM `Customers` WHERE `City` = @city + ) AS `m0` +) +"""); + } + + public override async Task SqlQueryRaw_with_dbParameter_mixed_in_subquery(bool async) + { + await base.SqlQueryRaw_with_dbParameter_mixed_in_subquery(async); + + AssertSql( +""" +p0='London' (Size = 4000) +@title='Sales Representative' (Nullable = false) +SELECT `m`.`CustomerID`, `m`.`EmployeeID`, `m`.`Freight`, `m`.`OrderDate`, `m`.`OrderID`, `m`.`RequiredDate`, `m`.`ShipAddress`, `m`.`ShipCity`, `m`.`ShipCountry`, `m`.`ShipName`, `m`.`ShipPostalCode`, `m`.`ShipRegion`, `m`.`ShipVia`, `m`.`ShippedDate` +FROM ( + SELECT * FROM `Orders` +) AS `m` +WHERE `m`.`CustomerID` IN ( + SELECT `m0`.`CustomerID` + FROM ( + SELECT * FROM `Customers` WHERE `City` = @p0 AND `ContactTitle` = @title + ) AS `m0` +) +""", + // + """ +@city='London' (Nullable = false) +p1='Sales Representative' (Size = 4000) +SELECT `m`.`CustomerID`, `m`.`EmployeeID`, `m`.`Freight`, `m`.`OrderDate`, `m`.`OrderID`, `m`.`RequiredDate`, `m`.`ShipAddress`, `m`.`ShipCity`, `m`.`ShipCountry`, `m`.`ShipName`, `m`.`ShipPostalCode`, `m`.`ShipRegion`, `m`.`ShipVia`, `m`.`ShippedDate` +FROM ( + SELECT * FROM `Orders` +) AS `m` +WHERE `m`.`CustomerID` IN ( + SELECT `m0`.`CustomerID` + FROM ( + SELECT * FROM `Customers` WHERE `City` = @city AND `ContactTitle` = @p1 + ) AS `m0` +) +"""); + } + + public override async Task Multiple_occurrences_of_SqlQuery_with_db_parameter_adds_parameter_only_once(bool async) + { + await base.Multiple_occurrences_of_SqlQuery_with_db_parameter_adds_parameter_only_once(async); + + AssertSql( +""" +city='Seattle' (Nullable = false) +SELECT `m`.`Address`, `m`.`City`, `m`.`CompanyName`, `m`.`ContactName`, `m`.`ContactTitle`, `m`.`Country`, `m`.`CustomerID`, `m`.`Fax`, `m`.`Phone`, `m`.`Region`, `m`.`PostalCode` +FROM ( + SELECT * FROM `Customers` WHERE `City` = @city +) AS `m` +INTERSECT +SELECT `m0`.`Address`, `m0`.`City`, `m0`.`CompanyName`, `m0`.`ContactName`, `m0`.`ContactTitle`, `m0`.`Country`, `m0`.`CustomerID`, `m0`.`Fax`, `m0`.`Phone`, `m0`.`Region`, `m0`.`PostalCode` +FROM ( + SELECT * FROM `Customers` WHERE `City` = @city +) AS `m0` +"""); + } + + [SupportedServerVersionCondition(nameof(ServerVersionSupport.CommonTableExpressions))] + public override async Task SqlQueryRaw_composed_with_common_table_expression(bool async) + { + await base.SqlQueryRaw_composed_with_common_table_expression(async); + + AssertSql( +""" +SELECT `m`.`Address`, `m`.`City`, `m`.`CompanyName`, `m`.`ContactName`, `m`.`ContactTitle`, `m`.`Country`, `m`.`CustomerID`, `m`.`Fax`, `m`.`Phone`, `m`.`Region`, `m`.`PostalCode` +FROM ( + WITH `Customers2` AS ( + SELECT * FROM `Customers` + ) + SELECT * FROM `Customers2` +) AS `m` +WHERE `m`.`ContactName` LIKE '%z%' +"""); + } + + protected override DbParameter CreateDbParameter(string name, object value) + => new SingleStoreParameter { ParameterName = name, Value = value }; + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/TPCFiltersInheritanceQuerySingleStoreFixture.cs b/test/EFCore.SingleStore.FunctionalTests/Query/TPCFiltersInheritanceQuerySingleStoreFixture.cs index 830391801..95997599f 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/TPCFiltersInheritanceQuerySingleStoreFixture.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/TPCFiltersInheritanceQuerySingleStoreFixture.cs @@ -2,6 +2,6 @@ namespace EntityFrameworkCore.SingleStore.FunctionalTests.Query; public class TPCFiltersInheritanceQuerySingleStoreFixture : TPCInheritanceQuerySingleStoreFixture { - protected override bool EnableFilters + public override bool EnableFilters => true; } diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/TPCFiltersInheritanceQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/TPCFiltersInheritanceQuerySingleStoreTest.cs index 5c466f349..d01c91691 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/TPCFiltersInheritanceQuerySingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/TPCFiltersInheritanceQuerySingleStoreTest.cs @@ -124,7 +124,7 @@ UNION ALL SELECT `k`.`Id`, `k`.`CountryId`, `k`.`Name`, `k`.`Species`, `k`.`EagleId`, `k`.`IsFlightless`, NULL AS `Group`, `k`.`FoundOn`, 'Kiwi' AS `Discriminator` FROM `Kiwi` AS `k` ) AS `t` -WHERE (`t`.`CountryId` = 1) AND (`t`.`CountryId` = 1) +WHERE `t`.`CountryId` = 1 ORDER BY `t`.`Species` """); } diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/TPCGearsOfWarQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/TPCGearsOfWarQuerySingleStoreTest.cs index 6f1ccdb11..2eef59dfc 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/TPCGearsOfWarQuerySingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/TPCGearsOfWarQuerySingleStoreTest.cs @@ -8,6 +8,7 @@ using Microsoft.EntityFrameworkCore.TestUtilities; using EntityFrameworkCore.SingleStore.FunctionalTests.TestUtilities; using EntityFrameworkCore.SingleStore.Infrastructure; +using EntityFrameworkCore.SingleStore.Tests; using EntityFrameworkCore.SingleStore.Tests.TestUtilities.Attributes; using Xunit; using Xunit.Abstractions; @@ -26,54 +27,6 @@ public TPCGearsOfWarQuerySingleStoreTest(TPCGearsOfWarQuerySingleStoreFixture fi protected override bool CanExecuteQueryString => true; - public override async Task Negate_on_binary_expression(bool async) - { - await base.Negate_on_binary_expression(async); - - AssertSql( -""" -SELECT `s`.`Id`, `s`.`Banner`, `s`.`Banner5`, `s`.`InternalNumber`, `s`.`Name` -FROM `Squads` AS `s` -WHERE `s`.`Id` = -(`s`.`Id` + `s`.`Id`) -"""); - } - - public override async Task Negate_on_column(bool async) - { - await base.Negate_on_column(async); - - AssertSql( -""" -SELECT `s`.`Id`, `s`.`Banner`, `s`.`Banner5`, `s`.`InternalNumber`, `s`.`Name` -FROM `Squads` AS `s` -WHERE `s`.`Id` = -`s`.`Id` -"""); - } - - public override async Task Double_negate_on_column(bool async) - { - await base.Double_negate_on_column(async); - - AssertSql( -""" -SELECT `s`.`Id`, `s`.`Banner`, `s`.`Banner5`, `s`.`InternalNumber`, `s`.`Name` -FROM `Squads` AS `s` -WHERE -(-`s`.`Id`) = `s`.`Id` -"""); - } - - public override async Task Negate_on_like_expression(bool async) - { - await base.Negate_on_like_expression(async); - - AssertSql( -""" -SELECT `s`.`Id`, `s`.`Banner`, `s`.`Banner5`, `s`.`InternalNumber`, `s`.`Name` -FROM `Squads` AS `s` -WHERE `s`.`Name` IS NOT NULL AND NOT (`s`.`Name` LIKE 'us%') -"""); - } - public override async Task Entity_equality_empty(bool async) { await base.Entity_equality_empty(async); @@ -1286,7 +1239,7 @@ UNION ALL ) AS `t` WHERE CASE WHEN `t`.`LeaderNickname` IS NULL THEN NULL - ELSE `t`.`LeaderNickname` IS NOT NULL AND (`t`.`LeaderNickname` LIKE '%us') + ELSE (`t`.`LeaderNickname` LIKE '%us') AND `t`.`LeaderNickname` IS NOT NULL END = TRUE """); } @@ -2023,7 +1976,7 @@ LIMIT 1 """); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override async Task Where_subquery_boolean(bool async) { await base.Where_subquery_boolean(async); @@ -2047,7 +2000,7 @@ ORDER BY `w`.`Id` """); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override async Task Where_subquery_boolean_with_pushdown(bool async) { await base.Where_subquery_boolean_with_pushdown(async); @@ -2180,7 +2133,7 @@ ORDER BY `t`.`Nickname` """); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override async Task Where_subquery_distinct_singleordefault_boolean2(bool async) { await base.Where_subquery_distinct_singleordefault_boolean2(async); @@ -2882,12 +2835,30 @@ public override async Task Non_unicode_string_literals_in_contains_is_used_for_n { await base.Non_unicode_string_literals_in_contains_is_used_for_non_unicode_column(async); - AssertSql( + if (SingleStoreTestHelpers.HasPrimitiveCollectionsSupport(Fixture)) + { + AssertSql( +""" +SELECT `c`.`Name`, `c`.`Location`, `c`.`Nation` +FROM `Cities` AS `c` +WHERE EXISTS ( + SELECT 1 + FROM JSON_TABLE('["Unknown","Jacinto\\u0027s location","Ephyra\\u0027s location"]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` longtext PATH '$[0]' + )) AS `c0` + WHERE (`c0`.`value` = `c`.`Location`) OR (`c0`.`value` IS NULL AND (`c`.`Location` IS NULL))) +"""); + } + else + { + AssertSql( """ SELECT `c`.`Name`, `c`.`Location`, `c`.`Nation` FROM `Cities` AS `c` WHERE `c`.`Location` IN ('Unknown', 'Jacinto''s location', 'Ephyra''s location') """); + } } public override async Task Non_unicode_string_literals_is_used_for_non_unicode_column_with_subquery(bool async) @@ -3333,7 +3304,7 @@ public override async Task Optional_navigation_type_compensation_works_with_bina AssertSql( """ -SELECT (`t0`.`HasSoulPatch` = TRUE) AND (`t`.`Note` LIKE '%Cole%') +SELECT (`t0`.`HasSoulPatch` = TRUE) AND ((`t`.`Note` LIKE '%Cole%') AND `t`.`Note` IS NOT NULL) FROM `Tags` AS `t` LEFT JOIN ( SELECT `g`.`Nickname`, `g`.`SquadId`, `g`.`HasSoulPatch` @@ -3515,16 +3486,13 @@ UNION ALL SELECT `o`.`Nickname`, `o`.`SquadId` FROM `Officers` AS `o` ) AS `t0` ON (`t`.`GearNickName` = `t0`.`Nickname`) AND (`t`.`GearSquadId` = `t0`.`SquadId`) -WHERE ((`t`.`Note` <> 'K.I.A.') OR `t`.`Note` IS NULL) AND EXISTS ( - SELECT 1 - FROM ( - SELECT `g0`.`Nickname`, `g0`.`SquadId`, `g0`.`AssignedCityName`, `g0`.`CityOfBirthName`, `g0`.`FullName`, `g0`.`HasSoulPatch`, `g0`.`LeaderNickname`, `g0`.`LeaderSquadId`, `g0`.`Rank`, 'Gear' AS `Discriminator` - FROM `Gears` AS `g0` - UNION ALL - SELECT `o0`.`Nickname`, `o0`.`SquadId`, `o0`.`AssignedCityName`, `o0`.`CityOfBirthName`, `o0`.`FullName`, `o0`.`HasSoulPatch`, `o0`.`LeaderNickname`, `o0`.`LeaderSquadId`, `o0`.`Rank`, 'Officer' AS `Discriminator` - FROM `Officers` AS `o0` - ) AS `t1` - WHERE `t1`.`SquadId` = `t0`.`SquadId`) +WHERE ((`t`.`Note` <> 'K.I.A.') OR `t`.`Note` IS NULL) AND `t0`.`SquadId` IN ( + SELECT `g0`.`SquadId` + FROM `Gears` AS `g0` + UNION ALL + SELECT `o0`.`SquadId` + FROM `Officers` AS `o0` +) """); } @@ -3808,13 +3776,17 @@ WHERE EXTRACT(day FROM `m`.`Timeline`) = 2 public override async Task Where_datetimeoffset_hour_component(bool async) { - await base.Where_datetimeoffset_hour_component(async); + await AssertQuery( + async, + ss => from m in ss.Set() + where m.Timeline.Hour == /* 10 */ 8 + select m); AssertSql( """ SELECT `m`.`Id`, `m`.`CodeName`, `m`.`Date`, `m`.`Duration`, `m`.`Rating`, `m`.`Time`, `m`.`Timeline` FROM `Missions` AS `m` -WHERE EXTRACT(hour FROM `m`.`Timeline`) = 10 +WHERE EXTRACT(hour FROM `m`.`Timeline`) = 8 """); } @@ -4057,7 +4029,7 @@ public override async Task Any_with_optional_navigation_as_subquery_predicate_is """ SELECT `s`.`Name` FROM `Squads` AS `s` -WHERE NOT (EXISTS ( +WHERE NOT EXISTS ( SELECT 1 FROM ( SELECT `g`.`Nickname`, `g`.`SquadId`, `g`.`AssignedCityName`, `g`.`CityOfBirthName`, `g`.`FullName`, `g`.`HasSoulPatch`, `g`.`LeaderNickname`, `g`.`LeaderSquadId`, `g`.`Rank`, 'Gear' AS `Discriminator` @@ -4067,7 +4039,7 @@ UNION ALL FROM `Officers` AS `o` ) AS `t` LEFT JOIN `Tags` AS `t0` ON (`t`.`Nickname` = `t0`.`GearNickName`) AND (`t`.`SquadId` = `t0`.`GearSquadId`) - WHERE (`s`.`Id` = `t`.`SquadId`) AND (`t0`.`Note` = 'Dom''s Tag'))) + WHERE (`s`.`Id` = `t`.`SquadId`) AND (`t0`.`Note` = 'Dom''s Tag')) """); } @@ -4095,12 +4067,30 @@ public override async Task Contains_with_local_nullable_guid_list_closure(bool a { await base.Contains_with_local_nullable_guid_list_closure(async); - AssertSql( + if (SingleStoreTestHelpers.HasPrimitiveCollectionsSupport(Fixture)) + { + AssertSql( +""" +SELECT `t`.`Id`, `t`.`GearNickName`, `t`.`GearSquadId`, `t`.`IssueDate`, `t`.`Note` +FROM `Tags` AS `t` +WHERE `t`.`Id` IN ( + SELECT `i`.`value` + FROM JSON_TABLE('["d2c26679-562b-44d1-ab96-23d1775e0926","23cbcf9b-ce14-45cf-aafa-2c2667ebfdd3","ab1b82d7-88db-42bd-a132-7eef9aa68af4"]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` char(36) PATH '$[0]' + )) AS `i` +) +"""); + } + else + { + AssertSql( """ SELECT `t`.`Id`, `t`.`GearNickName`, `t`.`GearSquadId`, `t`.`IssueDate`, `t`.`Note` FROM `Tags` AS `t` -WHERE `t`.`Id` IN ('d2c26679-562b-44d1-ab96-23d1775e0926', '23cbcf9b-ce14-45cf-aafa-2c2667ebfdd3', 'ab1b82d7-88db-42bd-a132-7eef9aa68af4') +WHERE `t`.`Id` IN ('df36f493-463f-4123-83f9-6b135deeb7ba', '23cbcf9b-ce14-45cf-aafa-2c2667ebfdd3', 'ab1b82d7-88db-42bd-a132-7eef9aa68af4') """); + } } public override async Task Unnecessary_include_doesnt_get_added_complex_when_projecting_EF_Property(bool async) @@ -4750,7 +4740,31 @@ public override async Task Contains_on_nullable_array_produces_correct_sql(bool { await base.Contains_on_nullable_array_produces_correct_sql(async); - AssertSql( + if (SingleStoreTestHelpers.HasPrimitiveCollectionsSupport(Fixture)) + { + AssertSql( +""" +SELECT `t`.`Nickname`, `t`.`SquadId`, `t`.`AssignedCityName`, `t`.`CityOfBirthName`, `t`.`FullName`, `t`.`HasSoulPatch`, `t`.`LeaderNickname`, `t`.`LeaderSquadId`, `t`.`Rank`, `t`.`Discriminator` +FROM ( + SELECT `g`.`Nickname`, `g`.`SquadId`, `g`.`AssignedCityName`, `g`.`CityOfBirthName`, `g`.`FullName`, `g`.`HasSoulPatch`, `g`.`LeaderNickname`, `g`.`LeaderSquadId`, `g`.`Rank`, 'Gear' AS `Discriminator` + FROM `Gears` AS `g` + UNION ALL + SELECT `o`.`Nickname`, `o`.`SquadId`, `o`.`AssignedCityName`, `o`.`CityOfBirthName`, `o`.`FullName`, `o`.`HasSoulPatch`, `o`.`LeaderNickname`, `o`.`LeaderSquadId`, `o`.`Rank`, 'Officer' AS `Discriminator` + FROM `Officers` AS `o` +) AS `t` +LEFT JOIN `Cities` AS `c` ON `t`.`AssignedCityName` = `c`.`Name` +WHERE (`t`.`SquadId` < 2) AND EXISTS ( + SELECT 1 + FROM JSON_TABLE('["Ephyra",null]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` varchar(255) PATH '$[0]' + )) AS `c0` + WHERE (`c0`.`value` = `c`.`Name`) OR (`c0`.`value` IS NULL AND (`c`.`Name` IS NULL))) +"""); + } + else + { + AssertSql( """ SELECT `t`.`Nickname`, `t`.`SquadId`, `t`.`AssignedCityName`, `t`.`CityOfBirthName`, `t`.`FullName`, `t`.`HasSoulPatch`, `t`.`LeaderNickname`, `t`.`LeaderSquadId`, `t`.`Rank`, `t`.`Discriminator` FROM ( @@ -4761,11 +4775,12 @@ UNION ALL FROM `Officers` AS `o` ) AS `t` LEFT JOIN `Cities` AS `c` ON `t`.`AssignedCityName` = `c`.`Name` -WHERE (`t`.`SquadId` < 2) AND ((`c`.`Name` = 'Ephyra') OR `c`.`Name` IS NULL) +WHERE (`t`.`SquadId` < 2) AND (`c`.`Name` IS NULL OR (`c`.`Name` = 'Ephyra')) """); + } } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override async Task Optional_navigation_with_collection_composite_key(bool async) { await base.Optional_navigation_with_collection_composite_key(async); @@ -5956,7 +5971,7 @@ UNION ALL """); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override async Task Multiple_orderby_with_navigation_expansion_on_one_of_the_order_bys_inside_subquery_complex_orderings( bool async) { @@ -6311,7 +6326,7 @@ LIMIT 18446744073709551610 OFFSET 0 """); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override async Task Correlated_collections_with_FirstOrDefault(bool async) { await base.Correlated_collections_with_FirstOrDefault(async); @@ -7225,7 +7240,7 @@ LEFT JOIN ( """); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override async Task Project_one_value_type_from_empty_collection(bool async) { await base.Project_one_value_type_from_empty_collection(async); @@ -7248,7 +7263,7 @@ UNION ALL """); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override async Task Project_one_value_type_converted_to_nullable_from_empty_collection(bool async) { await base.Project_one_value_type_converted_to_nullable_from_empty_collection(async); @@ -7298,7 +7313,7 @@ UNION ALL """); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override async Task Filter_on_subquery_projecting_one_value_type_from_empty_collection(bool async) { await base.Filter_on_subquery_projecting_one_value_type_from_empty_collection(async); @@ -7321,7 +7336,7 @@ UNION ALL """); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override async Task Select_subquery_projecting_single_constant_int(bool async) { await base.Select_subquery_projecting_single_constant_int(async); @@ -7343,7 +7358,7 @@ UNION ALL """); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override async Task Select_subquery_projecting_single_constant_string(bool async) { await base.Select_subquery_projecting_single_constant_string(async); @@ -7365,7 +7380,7 @@ UNION ALL """); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override async Task Select_subquery_projecting_single_constant_bool(bool async) { await base.Select_subquery_projecting_single_constant_bool(async); @@ -7692,7 +7707,7 @@ public override async Task Cast_to_derived_type_after_OfType_works(bool async) """); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override async Task Select_subquery_boolean(bool async) { await base.Select_subquery_boolean(async); @@ -7715,7 +7730,7 @@ UNION ALL """); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override async Task Select_subquery_boolean_with_pushdown(bool async) { await base.Select_subquery_boolean_with_pushdown(async); @@ -7738,7 +7753,7 @@ UNION ALL """); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override async Task Select_subquery_int_with_inside_cast_and_coalesce(bool async) { await base.Select_subquery_int_with_inside_cast_and_coalesce(async); @@ -7761,7 +7776,7 @@ UNION ALL """); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override async Task Select_subquery_int_with_outside_cast_and_coalesce(bool async) { await base.Select_subquery_int_with_outside_cast_and_coalesce(async); @@ -7784,7 +7799,7 @@ UNION ALL """); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override async Task Select_subquery_int_with_pushdown_and_coalesce(bool async) { await base.Select_subquery_int_with_pushdown_and_coalesce(async); @@ -7807,7 +7822,7 @@ UNION ALL """); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override async Task Select_subquery_int_with_pushdown_and_coalesce2(bool async) { await base.Select_subquery_int_with_pushdown_and_coalesce2(async); @@ -7835,7 +7850,7 @@ UNION ALL """); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override async Task Select_subquery_boolean_empty(bool async) { await base.Select_subquery_boolean_empty(async); @@ -7858,7 +7873,7 @@ UNION ALL """); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override async Task Select_subquery_boolean_empty_with_pushdown(bool async) { await base.Select_subquery_boolean_empty_with_pushdown(async); @@ -7907,7 +7922,7 @@ UNION ALL """); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override async Task Select_subquery_distinct_singleordefault_boolean2(bool async) { await base.Select_subquery_distinct_singleordefault_boolean2(async); @@ -7982,7 +7997,7 @@ UNION ALL """); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override async Task Select_subquery_distinct_singleordefault_boolean_empty2(bool async) { await base.Select_subquery_distinct_singleordefault_boolean_empty2(async); @@ -8075,7 +8090,31 @@ public override async Task Correlated_collection_with_complex_order_by_funcletiz { await base.Correlated_collection_with_complex_order_by_funcletized_to_constant_bool(async); - AssertSql( + if (SingleStoreTestHelpers.HasPrimitiveCollectionsSupport(Fixture)) + { + AssertSql( +""" +SELECT `t`.`Nickname`, `t`.`SquadId`, `w`.`Name`, `w`.`Id` +FROM ( + SELECT `g`.`Nickname`, `g`.`SquadId`, `g`.`FullName` + FROM `Gears` AS `g` + UNION ALL + SELECT `o`.`Nickname`, `o`.`SquadId`, `o`.`FullName` + FROM `Officers` AS `o` +) AS `t` +LEFT JOIN `Weapons` AS `w` ON `t`.`FullName` = `w`.`OwnerFullName` +ORDER BY COALESCE(`t`.`Nickname` IN ( + SELECT `n`.`value` + FROM JSON_TABLE('[]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` varchar(255) PATH '$[0]' + )) AS `n` +), FALSE) DESC, `t`.`Nickname`, `t`.`SquadId` +"""); + } + else + { + AssertSql( """ SELECT `t`.`Nickname`, `t`.`SquadId`, `w`.`Name`, `w`.`Id` FROM ( @@ -8088,6 +8127,7 @@ UNION ALL LEFT JOIN `Weapons` AS `w` ON `t`.`FullName` = `w`.`OwnerFullName` ORDER BY `t`.`Nickname`, `t`.`SquadId` """); + } } public override async Task Double_order_by_on_nullable_bool_coming_from_optional_navigation(bool async) @@ -8112,7 +8152,7 @@ public override async Task Double_order_by_on_Like(bool async) SELECT `w0`.`Id`, `w0`.`AmmunitionType`, `w0`.`IsAutomatic`, `w0`.`Name`, `w0`.`OwnerFullName`, `w0`.`SynergyWithId` FROM `Weapons` AS `w` LEFT JOIN `Weapons` AS `w0` ON `w`.`SynergyWithId` = `w0`.`Id` -ORDER BY `w0`.`Name` LIKE '%Lancer' +ORDER BY (`w0`.`Name` LIKE '%Lancer') AND `w0`.`Name` IS NOT NULL """); } @@ -8629,7 +8669,7 @@ SELECT 1 """); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override async Task Query_with_complex_let_containing_ordering_and_filter_projecting_firstOrDefault_element_of_let( bool async) { @@ -8943,7 +8983,27 @@ await AssertQuery( ss => ss.Set().Where( m => start <= m.Timeline.Date && m.Timeline < end && dates.Contains(m.Timeline))); - AssertSql( + if (SingleStoreTestHelpers.HasPrimitiveCollectionsSupport(Fixture)) + { + AssertSql( +""" +@__start_0='1902-01-01T10:00:00.1234567+01:30' +@__end_1='1902-01-03T10:00:00.1234567+01:30' + +SELECT `m`.`Id`, `m`.`CodeName`, `m`.`Date`, `m`.`Duration`, `m`.`Rating`, `m`.`Time`, `m`.`Timeline` +FROM `Missions` AS `m` +WHERE ((@__start_0 <= CONVERT(`m`.`Timeline`, date)) AND (`m`.`Timeline` < @__end_1)) AND `m`.`Timeline` IN ( + SELECT `d`.`value` + FROM JSON_TABLE('["1902-01-02T10:00:00.1234567+01:30"]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` datetime(6) PATH '$[0]' + )) AS `d` +) +"""); + } + else + { + AssertSql( """ @__start_0='1902-01-01T08:30:00.1234560+00:00' @__end_1='1902-01-03T08:30:00.1234560+00:00' @@ -8952,6 +9012,7 @@ await AssertQuery( FROM `Missions` AS `m` WHERE ((@__start_0 <= CONVERT(`m`.`Timeline`, date)) AND (`m`.`Timeline` < @__end_1)) AND (`m`.`Timeline` = '1902-01-02 08:30:00.123456') """); + } } [ConditionalTheory(Skip = "TODO: Does not work as expected, probably due to some test definition issues.")] @@ -9460,10 +9521,11 @@ UNION ALL FROM `Officers` AS `o` ) AS `t` INNER JOIN `Squads` AS `s` ON `t`.`SquadId` = `s`.`Id` - WHERE EXISTS ( - SELECT 1 + WHERE `s`.`Id` IN ( + SELECT `s0`.`Id` FROM `Squads` AS `s0` - WHERE (`s0`.`Id` = @__squadId_0) AND (`s0`.`Id` = `s`.`Id`)) + WHERE `s0`.`Id` = @__squadId_0 + ) UNION ALL SELECT `t1`.`Nickname`, `t1`.`SquadId`, `t1`.`AssignedCityName`, `t1`.`CityOfBirthName`, `t1`.`FullName`, `t1`.`HasSoulPatch`, `t1`.`LeaderNickname`, `t1`.`LeaderSquadId`, `t1`.`Rank`, `t1`.`Discriminator` FROM ( @@ -9474,10 +9536,11 @@ UNION ALL FROM `Officers` AS `o0` ) AS `t1` INNER JOIN `Squads` AS `s1` ON `t1`.`SquadId` = `s1`.`Id` - WHERE EXISTS ( - SELECT 1 + WHERE `s1`.`Id` IN ( + SELECT `s2`.`Id` FROM `Squads` AS `s2` - WHERE (`s2`.`Id` = @__squadId_0) AND (`s2`.`Id` = `s1`.`Id`)) + WHERE `s2`.`Id` = @__squadId_0 + ) ) AS `t0` ORDER BY `t0`.`FullName` """); @@ -9807,17 +9870,41 @@ public override async Task OrderBy_Contains_empty_list(bool async) { await base.OrderBy_Contains_empty_list(async); - AssertSql( -""" -SELECT `t`.`Nickname`, `t`.`SquadId`, `t`.`AssignedCityName`, `t`.`CityOfBirthName`, `t`.`FullName`, `t`.`HasSoulPatch`, `t`.`LeaderNickname`, `t`.`LeaderSquadId`, `t`.`Rank`, `t`.`Discriminator` -FROM ( - SELECT `g`.`Nickname`, `g`.`SquadId`, `g`.`AssignedCityName`, `g`.`CityOfBirthName`, `g`.`FullName`, `g`.`HasSoulPatch`, `g`.`LeaderNickname`, `g`.`LeaderSquadId`, `g`.`Rank`, 'Gear' AS `Discriminator` - FROM `Gears` AS `g` - UNION ALL - SELECT `o`.`Nickname`, `o`.`SquadId`, `o`.`AssignedCityName`, `o`.`CityOfBirthName`, `o`.`FullName`, `o`.`HasSoulPatch`, `o`.`LeaderNickname`, `o`.`LeaderSquadId`, `o`.`Rank`, 'Officer' AS `Discriminator` - FROM `Officers` AS `o` -) AS `t` -"""); + if (SingleStoreTestHelpers.HasPrimitiveCollectionsSupport(Fixture)) + { +AssertSql( + """ + SELECT `t`.`Nickname`, `t`.`SquadId`, `t`.`AssignedCityName`, `t`.`CityOfBirthName`, `t`.`FullName`, `t`.`HasSoulPatch`, `t`.`LeaderNickname`, `t`.`LeaderSquadId`, `t`.`Rank`, `t`.`Discriminator` + FROM ( + SELECT `g`.`Nickname`, `g`.`SquadId`, `g`.`AssignedCityName`, `g`.`CityOfBirthName`, `g`.`FullName`, `g`.`HasSoulPatch`, `g`.`LeaderNickname`, `g`.`LeaderSquadId`, `g`.`Rank`, 'Gear' AS `Discriminator` + FROM `Gears` AS `g` + UNION ALL + SELECT `o`.`Nickname`, `o`.`SquadId`, `o`.`AssignedCityName`, `o`.`CityOfBirthName`, `o`.`FullName`, `o`.`HasSoulPatch`, `o`.`LeaderNickname`, `o`.`LeaderSquadId`, `o`.`Rank`, 'Officer' AS `Discriminator` + FROM `Officers` AS `o` + ) AS `t` + ORDER BY `t`.`SquadId` IN ( + SELECT `i`.`value` + FROM JSON_TABLE('[]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `i` + ) + """); + } + else + { +AssertSql( + """ + SELECT `t`.`Nickname`, `t`.`SquadId`, `t`.`AssignedCityName`, `t`.`CityOfBirthName`, `t`.`FullName`, `t`.`HasSoulPatch`, `t`.`LeaderNickname`, `t`.`LeaderSquadId`, `t`.`Rank`, `t`.`Discriminator` + FROM ( + SELECT `g`.`Nickname`, `g`.`SquadId`, `g`.`AssignedCityName`, `g`.`CityOfBirthName`, `g`.`FullName`, `g`.`HasSoulPatch`, `g`.`LeaderNickname`, `g`.`LeaderSquadId`, `g`.`Rank`, 'Gear' AS `Discriminator` + FROM `Gears` AS `g` + UNION ALL + SELECT `o`.`Nickname`, `o`.`SquadId`, `o`.`AssignedCityName`, `o`.`CityOfBirthName`, `o`.`FullName`, `o`.`HasSoulPatch`, `o`.`LeaderNickname`, `o`.`LeaderSquadId`, `o`.`Rank`, 'Officer' AS `Discriminator` + FROM `Officers` AS `o` + ) AS `t` + """); + } } public override async Task Where_with_enum_flags_parameter(bool async) @@ -9883,31 +9970,37 @@ public override async Task FirstOrDefault_navigation_access_entity_equality_in_w AssertSql( """ -SELECT `l`.`Id`, `l`.`CapitalName`, `l`.`Name`, `l`.`ServerAddress`, `l`.`CommanderName`, `l`.`Eradicated` -FROM `LocustHordes` AS `l` -LEFT JOIN `Cities` AS `c` ON `l`.`CapitalName` = `c`.`Name` +SELECT `t`.`Nickname`, `t`.`SquadId`, `t`.`AssignedCityName`, `t`.`CityOfBirthName`, `t`.`FullName`, `t`.`HasSoulPatch`, `t`.`LeaderNickname`, `t`.`LeaderSquadId`, `t`.`Rank`, `t`.`Discriminator` +FROM ( + SELECT `g`.`Nickname`, `g`.`SquadId`, `g`.`AssignedCityName`, `g`.`CityOfBirthName`, `g`.`FullName`, `g`.`HasSoulPatch`, `g`.`LeaderNickname`, `g`.`LeaderSquadId`, `g`.`Rank`, 'Gear' AS `Discriminator` + FROM `Gears` AS `g` + UNION ALL + SELECT `o`.`Nickname`, `o`.`SquadId`, `o`.`AssignedCityName`, `o`.`CityOfBirthName`, `o`.`FullName`, `o`.`HasSoulPatch`, `o`.`LeaderNickname`, `o`.`LeaderSquadId`, `o`.`Rank`, 'Officer' AS `Discriminator` + FROM `Officers` AS `o` +) AS `t` +LEFT JOIN `Cities` AS `c` ON `t`.`AssignedCityName` = `c`.`Name` WHERE (`c`.`Name` = ( SELECT `c0`.`Name` FROM ( - SELECT `g`.`Nickname`, `g`.`SquadId`, `g`.`AssignedCityName`, `g`.`CityOfBirthName`, `g`.`FullName`, `g`.`HasSoulPatch`, `g`.`LeaderNickname`, `g`.`LeaderSquadId`, `g`.`Rank`, 'Gear' AS `Discriminator` - FROM `Gears` AS `g` + SELECT `g0`.`Nickname`, `g0`.`SquadId`, `g0`.`AssignedCityName`, `g0`.`CityOfBirthName`, `g0`.`FullName`, `g0`.`HasSoulPatch`, `g0`.`LeaderNickname`, `g0`.`LeaderSquadId`, `g0`.`Rank`, 'Gear' AS `Discriminator` + FROM `Gears` AS `g0` UNION ALL - SELECT `o`.`Nickname`, `o`.`SquadId`, `o`.`AssignedCityName`, `o`.`CityOfBirthName`, `o`.`FullName`, `o`.`HasSoulPatch`, `o`.`LeaderNickname`, `o`.`LeaderSquadId`, `o`.`Rank`, 'Officer' AS `Discriminator` - FROM `Officers` AS `o` - ) AS `t` - INNER JOIN `Cities` AS `c0` ON `t`.`CityOfBirthName` = `c0`.`Name` - ORDER BY `t`.`Nickname` + SELECT `o0`.`Nickname`, `o0`.`SquadId`, `o0`.`AssignedCityName`, `o0`.`CityOfBirthName`, `o0`.`FullName`, `o0`.`HasSoulPatch`, `o0`.`LeaderNickname`, `o0`.`LeaderSquadId`, `o0`.`Rank`, 'Officer' AS `Discriminator` + FROM `Officers` AS `o0` + ) AS `t0` + INNER JOIN `Cities` AS `c0` ON `t0`.`CityOfBirthName` = `c0`.`Name` + ORDER BY `t0`.`Nickname` LIMIT 1)) OR (`c`.`Name` IS NULL AND (( SELECT `c0`.`Name` FROM ( - SELECT `g`.`Nickname`, `g`.`SquadId`, `g`.`AssignedCityName`, `g`.`CityOfBirthName`, `g`.`FullName`, `g`.`HasSoulPatch`, `g`.`LeaderNickname`, `g`.`LeaderSquadId`, `g`.`Rank`, 'Gear' AS `Discriminator` - FROM `Gears` AS `g` + SELECT `g0`.`Nickname`, `g0`.`SquadId`, `g0`.`AssignedCityName`, `g0`.`CityOfBirthName`, `g0`.`FullName`, `g0`.`HasSoulPatch`, `g0`.`LeaderNickname`, `g0`.`LeaderSquadId`, `g0`.`Rank`, 'Gear' AS `Discriminator` + FROM `Gears` AS `g0` UNION ALL - SELECT `o`.`Nickname`, `o`.`SquadId`, `o`.`AssignedCityName`, `o`.`CityOfBirthName`, `o`.`FullName`, `o`.`HasSoulPatch`, `o`.`LeaderNickname`, `o`.`LeaderSquadId`, `o`.`Rank`, 'Officer' AS `Discriminator` - FROM `Officers` AS `o` - ) AS `t` - INNER JOIN `Cities` AS `c0` ON `t`.`CityOfBirthName` = `c0`.`Name` - ORDER BY `t`.`Nickname` + SELECT `o0`.`Nickname`, `o0`.`SquadId`, `o0`.`AssignedCityName`, `o0`.`CityOfBirthName`, `o0`.`FullName`, `o0`.`HasSoulPatch`, `o0`.`LeaderNickname`, `o0`.`LeaderSquadId`, `o0`.`Rank`, 'Officer' AS `Discriminator` + FROM `Officers` AS `o0` + ) AS `t0` + INNER JOIN `Cities` AS `c0` ON `t0`.`CityOfBirthName` = `c0`.`Name` + ORDER BY `t0`.`Nickname` LIMIT 1) IS NULL)) """); } @@ -10096,7 +10189,7 @@ public override async Task Byte_array_filter_by_length_literal(bool async) """ SELECT `s`.`Id`, `s`.`Banner`, `s`.`Banner5`, `s`.`InternalNumber`, `s`.`Name` FROM `Squads` AS `s` -WHERE LENGTH(`s`.`Banner`) = 1 +WHERE LENGTH(`s`.`Banner`) = 2 """); } @@ -10106,7 +10199,7 @@ public override async Task Byte_array_filter_by_length_parameter(bool async) AssertSql( """ -@__p_0='1' +@__p_0='2' SELECT `s`.`Id`, `s`.`Banner`, `s`.`Banner5`, `s`.`InternalNumber`, `s`.`Name` FROM `Squads` AS `s` @@ -10177,7 +10270,7 @@ ELSE FALSE """); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override async Task Conditional_expression_with_test_being_simplified_to_constant_complex(bool isAsync) { await base.Conditional_expression_with_test_being_simplified_to_constant_complex(isAsync); @@ -10185,7 +10278,7 @@ public override async Task Conditional_expression_with_test_being_simplified_to_ AssertSql( """ @__prm_0='True' -@__prm2_1='Dom's Lancer' (Size = 4000) +@__prm2_1='Marcus' Lancer' (Size = 4000) SELECT `t`.`Nickname`, `t`.`SquadId`, `t`.`AssignedCityName`, `t`.`CityOfBirthName`, `t`.`FullName`, `t`.`HasSoulPatch`, `t`.`LeaderNickname`, `t`.`LeaderSquadId`, `t`.`Rank`, `t`.`Discriminator` FROM ( @@ -10361,7 +10454,7 @@ UNION ALL SELECT `l0`.`Name`, `l0`.`LocustHordeId`, `l0`.`ThreatLevel`, `l0`.`ThreatLevelByte`, `l0`.`ThreatLevelNullableByte`, `l0`.`DefeatedByNickname`, `l0`.`DefeatedBySquadId`, `l0`.`HighCommandId`, 'LocustCommander' AS `Discriminator` FROM `LocustCommanders` AS `l0` ) AS `t` -WHERE CAST(`t`.`ThreatLevel` AS signed) >= (5 + CAST(`t`.`ThreatLevel` AS signed)) +WHERE CAST(`t`.`ThreatLevel` AS signed) <= (5 + CAST(`t`.`ThreatLevel` AS signed)) """); } @@ -10429,7 +10522,7 @@ public override async Task Where_TimeSpan_Minutes(bool async) """ SELECT `m`.`Id`, `m`.`CodeName`, `m`.`Date`, `m`.`Duration`, `m`.`Rating`, `m`.`Time`, `m`.`Timeline` FROM `Missions` AS `m` -WHERE EXTRACT(minute FROM `m`.`Duration`) = 1 +WHERE EXTRACT(minute FROM `m`.`Duration`) = 2 """); } @@ -10441,7 +10534,7 @@ public override async Task Where_TimeSpan_Seconds(bool async) """ SELECT `m`.`Id`, `m`.`CodeName`, `m`.`Date`, `m`.`Duration`, `m`.`Rating`, `m`.`Time`, `m`.`Timeline` FROM `Missions` AS `m` -WHERE EXTRACT(second FROM `m`.`Duration`) = 1 +WHERE EXTRACT(second FROM `m`.`Duration`) = 3 """); } @@ -10453,7 +10546,7 @@ public override async Task Where_TimeSpan_Milliseconds(bool async) """ SELECT `m`.`Id`, `m`.`CodeName`, `m`.`Date`, `m`.`Duration`, `m`.`Rating`, `m`.`Time`, `m`.`Timeline` FROM `Missions` AS `m` -WHERE (EXTRACT(microsecond FROM `m`.`Duration`)) DIV (1000) = 1 +WHERE (EXTRACT(microsecond FROM `m`.`Duration`)) DIV (1000) = 456 """); } @@ -10471,16 +10564,13 @@ UNION ALL SELECT `l0`.`Name`, `l0`.`LocustHordeId`, `l0`.`ThreatLevel`, `l0`.`ThreatLevelByte`, `l0`.`ThreatLevelNullableByte`, `l0`.`DefeatedByNickname`, `l0`.`DefeatedBySquadId`, `l0`.`HighCommandId`, 'LocustCommander' AS `Discriminator` FROM `LocustCommanders` AS `l0` ) AS `t` -WHERE EXISTS ( - SELECT 1 - FROM ( - SELECT `l1`.`Name`, `l1`.`LocustHordeId`, `l1`.`ThreatLevel`, `l1`.`ThreatLevelByte`, `l1`.`ThreatLevelNullableByte`, NULL AS `DefeatedByNickname`, NULL AS `DefeatedBySquadId`, NULL AS `HighCommandId`, 'LocustLeader' AS `Discriminator` - FROM `LocustLeaders` AS `l1` - UNION ALL - SELECT `l2`.`Name`, `l2`.`LocustHordeId`, `l2`.`ThreatLevel`, `l2`.`ThreatLevelByte`, `l2`.`ThreatLevelNullableByte`, `l2`.`DefeatedByNickname`, `l2`.`DefeatedBySquadId`, `l2`.`HighCommandId`, 'LocustCommander' AS `Discriminator` - FROM `LocustCommanders` AS `l2` - ) AS `t0` - WHERE `t0`.`ThreatLevelByte` = `t`.`ThreatLevelByte`) +WHERE `t`.`ThreatLevelByte` IN ( + SELECT `l1`.`ThreatLevelByte` + FROM `LocustLeaders` AS `l1` + UNION ALL + SELECT `l2`.`ThreatLevelByte` + FROM `LocustCommanders` AS `l2` +) """); } @@ -10501,10 +10591,10 @@ UNION ALL WHERE EXISTS ( SELECT 1 FROM ( - SELECT `l1`.`Name`, `l1`.`LocustHordeId`, `l1`.`ThreatLevel`, `l1`.`ThreatLevelByte`, `l1`.`ThreatLevelNullableByte`, NULL AS `DefeatedByNickname`, NULL AS `DefeatedBySquadId`, NULL AS `HighCommandId`, 'LocustLeader' AS `Discriminator` + SELECT `l1`.`ThreatLevelNullableByte` FROM `LocustLeaders` AS `l1` UNION ALL - SELECT `l2`.`Name`, `l2`.`LocustHordeId`, `l2`.`ThreatLevel`, `l2`.`ThreatLevelByte`, `l2`.`ThreatLevelNullableByte`, `l2`.`DefeatedByNickname`, `l2`.`DefeatedBySquadId`, `l2`.`HighCommandId`, 'LocustCommander' AS `Discriminator` + SELECT `l2`.`ThreatLevelNullableByte` FROM `LocustCommanders` AS `l2` ) AS `t0` WHERE (`t0`.`ThreatLevelNullableByte` = `t`.`ThreatLevelNullableByte`) OR (`t0`.`ThreatLevelNullableByte` IS NULL AND (`t`.`ThreatLevelNullableByte` IS NULL))) @@ -10528,10 +10618,10 @@ UNION ALL WHERE EXISTS ( SELECT 1 FROM ( - SELECT `l1`.`Name`, `l1`.`LocustHordeId`, `l1`.`ThreatLevel`, `l1`.`ThreatLevelByte`, `l1`.`ThreatLevelNullableByte`, NULL AS `DefeatedByNickname`, NULL AS `DefeatedBySquadId`, NULL AS `HighCommandId`, 'LocustLeader' AS `Discriminator` + SELECT `l1`.`ThreatLevelNullableByte` FROM `LocustLeaders` AS `l1` UNION ALL - SELECT `l2`.`Name`, `l2`.`LocustHordeId`, `l2`.`ThreatLevel`, `l2`.`ThreatLevelByte`, `l2`.`ThreatLevelNullableByte`, `l2`.`DefeatedByNickname`, `l2`.`DefeatedBySquadId`, `l2`.`HighCommandId`, 'LocustCommander' AS `Discriminator` + SELECT `l2`.`ThreatLevelNullableByte` FROM `LocustCommanders` AS `l2` ) AS `t0` WHERE `t0`.`ThreatLevelNullableByte` IS NULL) @@ -10555,10 +10645,10 @@ UNION ALL WHERE EXISTS ( SELECT 1 FROM ( - SELECT `l1`.`Name`, `l1`.`LocustHordeId`, `l1`.`ThreatLevel`, `l1`.`ThreatLevelByte`, `l1`.`ThreatLevelNullableByte`, NULL AS `DefeatedByNickname`, NULL AS `DefeatedBySquadId`, NULL AS `HighCommandId`, 'LocustLeader' AS `Discriminator` + SELECT `l1`.`ThreatLevelNullableByte` FROM `LocustLeaders` AS `l1` UNION ALL - SELECT `l2`.`Name`, `l2`.`LocustHordeId`, `l2`.`ThreatLevel`, `l2`.`ThreatLevelByte`, `l2`.`ThreatLevelNullableByte`, `l2`.`DefeatedByNickname`, `l2`.`DefeatedBySquadId`, `l2`.`HighCommandId`, 'LocustCommander' AS `Discriminator` + SELECT `l2`.`ThreatLevelNullableByte` FROM `LocustCommanders` AS `l2` ) AS `t0` WHERE `t0`.`ThreatLevelNullableByte` IS NULL) @@ -10608,16 +10698,13 @@ UNION ALL SELECT `o`.`Nickname`, `o`.`SquadId`, `o`.`AssignedCityName`, `o`.`CityOfBirthName`, `o`.`FullName`, `o`.`HasSoulPatch`, `o`.`LeaderNickname`, `o`.`LeaderSquadId`, `o`.`Rank`, 'Officer' AS `Discriminator` FROM `Officers` AS `o` ) AS `t0` - WHERE EXISTS ( - SELECT 1 - FROM ( - SELECT `l1`.`Name`, `l1`.`LocustHordeId`, `l1`.`ThreatLevel`, `l1`.`ThreatLevelByte`, `l1`.`ThreatLevelNullableByte`, NULL AS `DefeatedByNickname`, NULL AS `DefeatedBySquadId`, NULL AS `HighCommandId`, 'LocustLeader' AS `Discriminator` - FROM `LocustLeaders` AS `l1` - UNION ALL - SELECT `l2`.`Name`, `l2`.`LocustHordeId`, `l2`.`ThreatLevel`, `l2`.`ThreatLevelByte`, `l2`.`ThreatLevelNullableByte`, `l2`.`DefeatedByNickname`, `l2`.`DefeatedBySquadId`, `l2`.`HighCommandId`, 'LocustCommander' AS `Discriminator` - FROM `LocustCommanders` AS `l2` - ) AS `t2` - WHERE `t2`.`ThreatLevelByte` = `t`.`ThreatLevelByte`) + WHERE `t`.`ThreatLevelByte` IN ( + SELECT `l1`.`ThreatLevelByte` + FROM `LocustLeaders` AS `l1` + UNION ALL + SELECT `l2`.`ThreatLevelByte` + FROM `LocustCommanders` AS `l2` + ) ) AS `t1` ON TRUE """); } @@ -10646,16 +10733,13 @@ UNION ALL SELECT `o`.`Nickname`, `o`.`SquadId`, `o`.`AssignedCityName`, `o`.`CityOfBirthName`, `o`.`FullName`, `o`.`HasSoulPatch`, `o`.`LeaderNickname`, `o`.`LeaderSquadId`, `o`.`Rank`, 'Officer' AS `Discriminator` FROM `Officers` AS `o` ) AS `t0` - WHERE NOT (EXISTS ( - SELECT 1 - FROM ( - SELECT `l1`.`Name`, `l1`.`LocustHordeId`, `l1`.`ThreatLevel`, `l1`.`ThreatLevelByte`, `l1`.`ThreatLevelNullableByte`, NULL AS `DefeatedByNickname`, NULL AS `DefeatedBySquadId`, NULL AS `HighCommandId`, 'LocustLeader' AS `Discriminator` - FROM `LocustLeaders` AS `l1` - UNION ALL - SELECT `l2`.`Name`, `l2`.`LocustHordeId`, `l2`.`ThreatLevel`, `l2`.`ThreatLevelByte`, `l2`.`ThreatLevelNullableByte`, `l2`.`DefeatedByNickname`, `l2`.`DefeatedBySquadId`, `l2`.`HighCommandId`, 'LocustCommander' AS `Discriminator` - FROM `LocustCommanders` AS `l2` - ) AS `t2` - WHERE `t2`.`ThreatLevelByte` = `t`.`ThreatLevelByte`)) + WHERE `t`.`ThreatLevelByte` NOT IN ( + SELECT `l1`.`ThreatLevelByte` + FROM `LocustLeaders` AS `l1` + UNION ALL + SELECT `l2`.`ThreatLevelByte` + FROM `LocustCommanders` AS `l2` + ) ) AS `t1` ON TRUE """); } @@ -10686,10 +10770,10 @@ UNION ALL WHERE EXISTS ( SELECT 1 FROM ( - SELECT `l1`.`Name`, `l1`.`LocustHordeId`, `l1`.`ThreatLevel`, `l1`.`ThreatLevelByte`, `l1`.`ThreatLevelNullableByte`, NULL AS `DefeatedByNickname`, NULL AS `DefeatedBySquadId`, NULL AS `HighCommandId`, 'LocustLeader' AS `Discriminator` + SELECT `l1`.`ThreatLevelNullableByte` FROM `LocustLeaders` AS `l1` UNION ALL - SELECT `l2`.`Name`, `l2`.`LocustHordeId`, `l2`.`ThreatLevel`, `l2`.`ThreatLevelByte`, `l2`.`ThreatLevelNullableByte`, `l2`.`DefeatedByNickname`, `l2`.`DefeatedBySquadId`, `l2`.`HighCommandId`, 'LocustCommander' AS `Discriminator` + SELECT `l2`.`ThreatLevelNullableByte` FROM `LocustCommanders` AS `l2` ) AS `t2` WHERE (`t2`.`ThreatLevelNullableByte` = `t`.`ThreatLevelNullableByte`) OR (`t2`.`ThreatLevelNullableByte` IS NULL AND (`t`.`ThreatLevelNullableByte` IS NULL))) @@ -10720,16 +10804,16 @@ UNION ALL SELECT `o`.`Nickname`, `o`.`SquadId`, `o`.`AssignedCityName`, `o`.`CityOfBirthName`, `o`.`FullName`, `o`.`HasSoulPatch`, `o`.`LeaderNickname`, `o`.`LeaderSquadId`, `o`.`Rank`, 'Officer' AS `Discriminator` FROM `Officers` AS `o` ) AS `t0` - WHERE NOT (EXISTS ( + WHERE NOT EXISTS ( SELECT 1 FROM ( - SELECT `l1`.`Name`, `l1`.`LocustHordeId`, `l1`.`ThreatLevel`, `l1`.`ThreatLevelByte`, `l1`.`ThreatLevelNullableByte`, NULL AS `DefeatedByNickname`, NULL AS `DefeatedBySquadId`, NULL AS `HighCommandId`, 'LocustLeader' AS `Discriminator` + SELECT `l1`.`ThreatLevelNullableByte` FROM `LocustLeaders` AS `l1` UNION ALL - SELECT `l2`.`Name`, `l2`.`LocustHordeId`, `l2`.`ThreatLevel`, `l2`.`ThreatLevelByte`, `l2`.`ThreatLevelNullableByte`, `l2`.`DefeatedByNickname`, `l2`.`DefeatedBySquadId`, `l2`.`HighCommandId`, 'LocustCommander' AS `Discriminator` + SELECT `l2`.`ThreatLevelNullableByte` FROM `LocustCommanders` AS `l2` ) AS `t2` - WHERE (`t2`.`ThreatLevelNullableByte` = `t`.`ThreatLevelNullableByte`) OR (`t2`.`ThreatLevelNullableByte` IS NULL AND (`t`.`ThreatLevelNullableByte` IS NULL)))) + WHERE (`t2`.`ThreatLevelNullableByte` = `t`.`ThreatLevelNullableByte`) OR (`t2`.`ThreatLevelNullableByte` IS NULL AND (`t`.`ThreatLevelNullableByte` IS NULL))) ) AS `t1` ON TRUE """); } @@ -10813,13 +10897,32 @@ public override async Task Enum_array_contains(bool async) { await base.Enum_array_contains(async); - AssertSql( + if (SingleStoreTestHelpers.HasPrimitiveCollectionsSupport(Fixture)) + { + AssertSql( +""" +SELECT `w`.`Id`, `w`.`AmmunitionType`, `w`.`IsAutomatic`, `w`.`Name`, `w`.`OwnerFullName`, `w`.`SynergyWithId` +FROM `Weapons` AS `w` +LEFT JOIN `Weapons` AS `w0` ON `w`.`SynergyWithId` = `w0`.`Id` +WHERE `w0`.`Id` IS NOT NULL AND EXISTS ( + SELECT 1 + FROM JSON_TABLE('[null,1]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` int PATH '$[0]' + )) AS `t` + WHERE (`t`.`value` = `w0`.`AmmunitionType`) OR (`t`.`value` IS NULL AND (`w0`.`AmmunitionType` IS NULL))) +"""); + } + else + { + AssertSql( """ SELECT `w`.`Id`, `w`.`AmmunitionType`, `w`.`IsAutomatic`, `w`.`Name`, `w`.`OwnerFullName`, `w`.`SynergyWithId` FROM `Weapons` AS `w` LEFT JOIN `Weapons` AS `w0` ON `w`.`SynergyWithId` = `w0`.`Id` -WHERE `w0`.`Id` IS NOT NULL AND ((`w0`.`AmmunitionType` = 1) OR `w0`.`AmmunitionType` IS NULL) +WHERE `w0`.`Id` IS NOT NULL AND (`w0`.`AmmunitionType` IS NULL OR (`w0`.`AmmunitionType` = 1)) """); + } } public override async Task CompareTo_used_with_non_unicode_string_column_and_constant(bool async) @@ -11028,7 +11131,7 @@ LIMIT 3 """); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override async Task FirstOrDefault_over_int_compared_to_zero(bool async) { await base.FirstOrDefault_over_int_compared_to_zero(async); @@ -11037,7 +11140,7 @@ public override async Task FirstOrDefault_over_int_compared_to_zero(bool async) """ SELECT `s`.`Name` FROM `Squads` AS `s` -WHERE (`s`.`Name` = 'Kilo') AND (COALESCE(( +WHERE (`s`.`Name` = 'Delta') AND (COALESCE(( SELECT `t`.`SquadId` FROM ( SELECT `g`.`Nickname`, `g`.`SquadId`, `g`.`AssignedCityName`, `g`.`CityOfBirthName`, `g`.`FullName`, `g`.`HasSoulPatch`, `g`.`LeaderNickname`, `g`.`LeaderSquadId`, `g`.`Rank`, 'Gear' AS `Discriminator` @@ -11047,6 +11150,7 @@ UNION ALL FROM `Officers` AS `o` ) AS `t` WHERE (`s`.`Id` = `t`.`SquadId`) AND (`t`.`HasSoulPatch` = TRUE) + ORDER BY `t`.`FullName` LIMIT 1), 0) <> 0) """); } @@ -11886,7 +11990,30 @@ public override async Task Where_bool_column_and_Contains(bool async) { await base.Where_bool_column_and_Contains(async); - AssertSql( + if (SingleStoreTestHelpers.HasPrimitiveCollectionsSupport(Fixture)) + { + AssertSql( +""" +SELECT `t`.`Nickname`, `t`.`SquadId`, `t`.`AssignedCityName`, `t`.`CityOfBirthName`, `t`.`FullName`, `t`.`HasSoulPatch`, `t`.`LeaderNickname`, `t`.`LeaderSquadId`, `t`.`Rank`, `t`.`Discriminator` +FROM ( + SELECT `g`.`Nickname`, `g`.`SquadId`, `g`.`AssignedCityName`, `g`.`CityOfBirthName`, `g`.`FullName`, `g`.`HasSoulPatch`, `g`.`LeaderNickname`, `g`.`LeaderSquadId`, `g`.`Rank`, 'Gear' AS `Discriminator` + FROM `Gears` AS `g` + UNION ALL + SELECT `o`.`Nickname`, `o`.`SquadId`, `o`.`AssignedCityName`, `o`.`CityOfBirthName`, `o`.`FullName`, `o`.`HasSoulPatch`, `o`.`LeaderNickname`, `o`.`LeaderSquadId`, `o`.`Rank`, 'Officer' AS `Discriminator` + FROM `Officers` AS `o` +) AS `t` +WHERE (`t`.`HasSoulPatch` = TRUE) AND `t`.`HasSoulPatch` IN ( + SELECT `v`.`value` + FROM JSON_TABLE('[false,true]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` tinyint(1) PATH '$[0]' + )) AS `v` +) +"""); + } + else + { + AssertSql( """ SELECT `t`.`Nickname`, `t`.`SquadId`, `t`.`AssignedCityName`, `t`.`CityOfBirthName`, `t`.`FullName`, `t`.`HasSoulPatch`, `t`.`LeaderNickname`, `t`.`LeaderSquadId`, `t`.`Rank`, `t`.`Discriminator` FROM ( @@ -11898,13 +12025,37 @@ UNION ALL ) AS `t` WHERE (`t`.`HasSoulPatch` = TRUE) AND `t`.`HasSoulPatch` IN (FALSE, TRUE) """); + } } public override async Task Where_bool_column_or_Contains(bool async) { await base.Where_bool_column_or_Contains(async); - AssertSql( + if (SingleStoreTestHelpers.HasPrimitiveCollectionsSupport(Fixture)) + { + AssertSql( +""" +SELECT `t`.`Nickname`, `t`.`SquadId`, `t`.`AssignedCityName`, `t`.`CityOfBirthName`, `t`.`FullName`, `t`.`HasSoulPatch`, `t`.`LeaderNickname`, `t`.`LeaderSquadId`, `t`.`Rank`, `t`.`Discriminator` +FROM ( + SELECT `g`.`Nickname`, `g`.`SquadId`, `g`.`AssignedCityName`, `g`.`CityOfBirthName`, `g`.`FullName`, `g`.`HasSoulPatch`, `g`.`LeaderNickname`, `g`.`LeaderSquadId`, `g`.`Rank`, 'Gear' AS `Discriminator` + FROM `Gears` AS `g` + UNION ALL + SELECT `o`.`Nickname`, `o`.`SquadId`, `o`.`AssignedCityName`, `o`.`CityOfBirthName`, `o`.`FullName`, `o`.`HasSoulPatch`, `o`.`LeaderNickname`, `o`.`LeaderSquadId`, `o`.`Rank`, 'Officer' AS `Discriminator` + FROM `Officers` AS `o` +) AS `t` +WHERE (`t`.`HasSoulPatch` = TRUE) AND `t`.`HasSoulPatch` IN ( + SELECT `v`.`value` + FROM JSON_TABLE('[false,true]', '$[*]' COLUMNS ( + `key` FOR ORDINALITY, + `value` tinyint(1) PATH '$[0]' + )) AS `v` +) +"""); + } + else + { + AssertSql( """ SELECT `t`.`Nickname`, `t`.`SquadId`, `t`.`AssignedCityName`, `t`.`CityOfBirthName`, `t`.`FullName`, `t`.`HasSoulPatch`, `t`.`LeaderNickname`, `t`.`LeaderSquadId`, `t`.`Rank`, `t`.`Discriminator` FROM ( @@ -11916,6 +12067,7 @@ UNION ALL ) AS `t` WHERE (`t`.`HasSoulPatch` = TRUE) AND `t`.`HasSoulPatch` IN (FALSE, TRUE) """); + } } public override async Task Enum_matching_take_value_gets_different_type_mapping(bool async) @@ -12131,7 +12283,7 @@ public override async Task Comparison_with_value_converted_subclass(bool async) """); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override async Task FirstOrDefault_on_empty_collection_of_DateTime_in_subquery(bool async) { await base.FirstOrDefault_on_empty_collection_of_DateTime_in_subquery(async); @@ -12854,7 +13006,7 @@ public override async Task Where_subquery_equality_to_null_with_composite_key(bo """ SELECT `s`.`Id`, `s`.`Banner`, `s`.`Banner5`, `s`.`InternalNumber`, `s`.`Name` FROM `Squads` AS `s` -WHERE NOT (EXISTS ( +WHERE NOT EXISTS ( SELECT 1 FROM ( SELECT `g`.`Nickname`, `g`.`SquadId`, `g`.`AssignedCityName`, `g`.`CityOfBirthName`, `g`.`FullName`, `g`.`HasSoulPatch`, `g`.`LeaderNickname`, `g`.`LeaderSquadId`, `g`.`Rank`, 'Gear' AS `Discriminator` @@ -12863,7 +13015,7 @@ UNION ALL SELECT `o`.`Nickname`, `o`.`SquadId`, `o`.`AssignedCityName`, `o`.`CityOfBirthName`, `o`.`FullName`, `o`.`HasSoulPatch`, `o`.`LeaderNickname`, `o`.`LeaderSquadId`, `o`.`Rank`, 'Officer' AS `Discriminator` FROM `Officers` AS `o` ) AS `t` - WHERE `s`.`Id` = `t`.`SquadId`)) + WHERE `s`.`Id` = `t`.`SquadId`) """); } @@ -12881,10 +13033,10 @@ UNION ALL SELECT `o`.`Nickname`, `o`.`SquadId`, `o`.`AssignedCityName`, `o`.`CityOfBirthName`, `o`.`FullName`, `o`.`HasSoulPatch`, `o`.`LeaderNickname`, `o`.`LeaderSquadId`, `o`.`Rank`, 'Officer' AS `Discriminator` FROM `Officers` AS `o` ) AS `t` -WHERE NOT (EXISTS ( +WHERE NOT EXISTS ( SELECT 1 FROM `Weapons` AS `w` - WHERE `t`.`FullName` = `w`.`OwnerFullName`)) + WHERE `t`.`FullName` = `w`.`OwnerFullName`) """); } @@ -12959,12 +13111,225 @@ UNION ALL """); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override Task Set_operator_with_navigation_in_projection_groupby_aggregate(bool async) { return base.Set_operator_with_navigation_in_projection_groupby_aggregate(async); } + public override async Task ToString_string_property_projection(bool async) + { + await base.ToString_string_property_projection(async); + + AssertSql( +""" +SELECT `w`.`Name` +FROM `Weapons` AS `w` +"""); + } + + public override async Task ElementAt_basic_with_OrderBy(bool async) + { + await base.ElementAt_basic_with_OrderBy(async); + + AssertSql( +""" +@__p_0='0' + +SELECT `t`.`Nickname`, `t`.`SquadId`, `t`.`AssignedCityName`, `t`.`CityOfBirthName`, `t`.`FullName`, `t`.`HasSoulPatch`, `t`.`LeaderNickname`, `t`.`LeaderSquadId`, `t`.`Rank`, `t`.`Discriminator` +FROM ( + SELECT `g`.`Nickname`, `g`.`SquadId`, `g`.`AssignedCityName`, `g`.`CityOfBirthName`, `g`.`FullName`, `g`.`HasSoulPatch`, `g`.`LeaderNickname`, `g`.`LeaderSquadId`, `g`.`Rank`, 'Gear' AS `Discriminator` + FROM `Gears` AS `g` + UNION ALL + SELECT `o`.`Nickname`, `o`.`SquadId`, `o`.`AssignedCityName`, `o`.`CityOfBirthName`, `o`.`FullName`, `o`.`HasSoulPatch`, `o`.`LeaderNickname`, `o`.`LeaderSquadId`, `o`.`Rank`, 'Officer' AS `Discriminator` + FROM `Officers` AS `o` +) AS `t` +ORDER BY `t`.`FullName` +LIMIT 1 OFFSET @__p_0 +"""); + } + + public override async Task ElementAtOrDefault_basic_with_OrderBy(bool async) + { + await base.ElementAtOrDefault_basic_with_OrderBy(async); + + AssertSql( +""" +@__p_0='1' + +SELECT `t`.`Nickname`, `t`.`SquadId`, `t`.`AssignedCityName`, `t`.`CityOfBirthName`, `t`.`FullName`, `t`.`HasSoulPatch`, `t`.`LeaderNickname`, `t`.`LeaderSquadId`, `t`.`Rank`, `t`.`Discriminator` +FROM ( + SELECT `g`.`Nickname`, `g`.`SquadId`, `g`.`AssignedCityName`, `g`.`CityOfBirthName`, `g`.`FullName`, `g`.`HasSoulPatch`, `g`.`LeaderNickname`, `g`.`LeaderSquadId`, `g`.`Rank`, 'Gear' AS `Discriminator` + FROM `Gears` AS `g` + UNION ALL + SELECT `o`.`Nickname`, `o`.`SquadId`, `o`.`AssignedCityName`, `o`.`CityOfBirthName`, `o`.`FullName`, `o`.`HasSoulPatch`, `o`.`LeaderNickname`, `o`.`LeaderSquadId`, `o`.`Rank`, 'Officer' AS `Discriminator` + FROM `Officers` AS `o` +) AS `t` +ORDER BY `t`.`FullName` +LIMIT 1 OFFSET @__p_0 +"""); + } + + public override async Task ElementAtOrDefault_basic_with_OrderBy_parameter(bool async) + { + await base.ElementAtOrDefault_basic_with_OrderBy_parameter(async); + + AssertSql( +""" +@__p_0='2' + +SELECT `t`.`Nickname`, `t`.`SquadId`, `t`.`AssignedCityName`, `t`.`CityOfBirthName`, `t`.`FullName`, `t`.`HasSoulPatch`, `t`.`LeaderNickname`, `t`.`LeaderSquadId`, `t`.`Rank`, `t`.`Discriminator` +FROM ( + SELECT `g`.`Nickname`, `g`.`SquadId`, `g`.`AssignedCityName`, `g`.`CityOfBirthName`, `g`.`FullName`, `g`.`HasSoulPatch`, `g`.`LeaderNickname`, `g`.`LeaderSquadId`, `g`.`Rank`, 'Gear' AS `Discriminator` + FROM `Gears` AS `g` + UNION ALL + SELECT `o`.`Nickname`, `o`.`SquadId`, `o`.`AssignedCityName`, `o`.`CityOfBirthName`, `o`.`FullName`, `o`.`HasSoulPatch`, `o`.`LeaderNickname`, `o`.`LeaderSquadId`, `o`.`Rank`, 'Officer' AS `Discriminator` + FROM `Officers` AS `o` +) AS `t` +ORDER BY `t`.`FullName` +LIMIT 1 OFFSET @__p_0 +"""); + } + + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed.")] + public override async Task Where_subquery_with_ElementAtOrDefault_equality_to_null_with_composite_key(bool async) + { + await base.Where_subquery_with_ElementAtOrDefault_equality_to_null_with_composite_key(async); + + AssertSql( +""" +SELECT `s`.`Id`, `s`.`Banner`, `s`.`Banner5`, `s`.`InternalNumber`, `s`.`Name` +FROM `Squads` AS `s` +WHERE NOT EXISTS ( + SELECT 1 + FROM ( + SELECT `g`.`Nickname`, `g`.`SquadId`, `g`.`AssignedCityName`, `g`.`CityOfBirthName`, `g`.`FullName`, `g`.`HasSoulPatch`, `g`.`LeaderNickname`, `g`.`LeaderSquadId`, `g`.`Rank`, 'Gear' AS `Discriminator` + FROM `Gears` AS `g` + UNION ALL + SELECT `o`.`Nickname`, `o`.`SquadId`, `o`.`AssignedCityName`, `o`.`CityOfBirthName`, `o`.`FullName`, `o`.`HasSoulPatch`, `o`.`LeaderNickname`, `o`.`LeaderSquadId`, `o`.`Rank`, 'Officer' AS `Discriminator` + FROM `Officers` AS `o` + ) AS `t` + WHERE `s`.`Id` = `t`.`SquadId` + ORDER BY `t`.`Nickname` + LIMIT 18446744073709551610 OFFSET 2) +"""); + } + + [SupportedServerVersionCondition(nameof(ServerVersionSupport.LimitWithNonConstantValue))] + public override async Task Where_subquery_with_ElementAt_using_column_as_index(bool async) + { + await base.Where_subquery_with_ElementAt_using_column_as_index(async); + + AssertSql(""); + } + + public override async Task Using_indexer_on_byte_array_and_string_in_projection(bool async) + { + await base.Using_indexer_on_byte_array_and_string_in_projection(async); + + AssertSql( +""" +SELECT `s`.`Id`, ASCII(SUBSTRING(`s`.`Banner`, 0 + 1, 1)), `s`.`Name` +FROM `Squads` AS `s` +"""); + } + + public override async Task DateTimeOffset_to_unix_time_milliseconds(bool async) + { + await base.DateTimeOffset_to_unix_time_milliseconds(async); + + AssertSql( +""" +@__unixEpochMilliseconds_0='0' + +SELECT `t`.`Nickname`, `t`.`SquadId`, `t`.`AssignedCityName`, `t`.`CityOfBirthName`, `t`.`FullName`, `t`.`HasSoulPatch`, `t`.`LeaderNickname`, `t`.`LeaderSquadId`, `t`.`Rank`, `t`.`Discriminator`, `s`.`Id`, `s`.`Banner`, `s`.`Banner5`, `s`.`InternalNumber`, `s`.`Name`, `s1`.`SquadId`, `s1`.`MissionId` +FROM ( + SELECT `g`.`Nickname`, `g`.`SquadId`, `g`.`AssignedCityName`, `g`.`CityOfBirthName`, `g`.`FullName`, `g`.`HasSoulPatch`, `g`.`LeaderNickname`, `g`.`LeaderSquadId`, `g`.`Rank`, 'Gear' AS `Discriminator` + FROM `Gears` AS `g` + UNION ALL + SELECT `o`.`Nickname`, `o`.`SquadId`, `o`.`AssignedCityName`, `o`.`CityOfBirthName`, `o`.`FullName`, `o`.`HasSoulPatch`, `o`.`LeaderNickname`, `o`.`LeaderSquadId`, `o`.`Rank`, 'Officer' AS `Discriminator` + FROM `Officers` AS `o` +) AS `t` +INNER JOIN `Squads` AS `s` ON `t`.`SquadId` = `s`.`Id` +LEFT JOIN `SquadMissions` AS `s1` ON `s`.`Id` = `s1`.`SquadId` +WHERE NOT EXISTS ( + SELECT 1 + FROM `SquadMissions` AS `s0` + INNER JOIN `Missions` AS `m` ON `s0`.`MissionId` = `m`.`Id` + WHERE (`s`.`Id` = `s0`.`SquadId`) AND (@__unixEpochMilliseconds_0 = (TIMESTAMPDIFF(microsecond, '1970-01-01 00:00:00', `m`.`Timeline`)) DIV (1000))) +ORDER BY `t`.`Nickname`, `t`.`SquadId`, `s`.`Id`, `s1`.`SquadId` +"""); + } + + public override async Task DateTimeOffset_to_unix_time_seconds(bool async) + { + await base.DateTimeOffset_to_unix_time_seconds(async); + + AssertSql( +""" +@__unixEpochSeconds_0='0' + +SELECT `t`.`Nickname`, `t`.`SquadId`, `t`.`AssignedCityName`, `t`.`CityOfBirthName`, `t`.`FullName`, `t`.`HasSoulPatch`, `t`.`LeaderNickname`, `t`.`LeaderSquadId`, `t`.`Rank`, `t`.`Discriminator`, `s`.`Id`, `s`.`Banner`, `s`.`Banner5`, `s`.`InternalNumber`, `s`.`Name`, `s1`.`SquadId`, `s1`.`MissionId` +FROM ( + SELECT `g`.`Nickname`, `g`.`SquadId`, `g`.`AssignedCityName`, `g`.`CityOfBirthName`, `g`.`FullName`, `g`.`HasSoulPatch`, `g`.`LeaderNickname`, `g`.`LeaderSquadId`, `g`.`Rank`, 'Gear' AS `Discriminator` + FROM `Gears` AS `g` + UNION ALL + SELECT `o`.`Nickname`, `o`.`SquadId`, `o`.`AssignedCityName`, `o`.`CityOfBirthName`, `o`.`FullName`, `o`.`HasSoulPatch`, `o`.`LeaderNickname`, `o`.`LeaderSquadId`, `o`.`Rank`, 'Officer' AS `Discriminator` + FROM `Officers` AS `o` +) AS `t` +INNER JOIN `Squads` AS `s` ON `t`.`SquadId` = `s`.`Id` +LEFT JOIN `SquadMissions` AS `s1` ON `s`.`Id` = `s1`.`SquadId` +WHERE NOT EXISTS ( + SELECT 1 + FROM `SquadMissions` AS `s0` + INNER JOIN `Missions` AS `m` ON `s0`.`MissionId` = `m`.`Id` + WHERE (`s`.`Id` = `s0`.`SquadId`) AND (@__unixEpochSeconds_0 = TIMESTAMPDIFF(second, '1970-01-01 00:00:00', `m`.`Timeline`))) +ORDER BY `t`.`Nickname`, `t`.`SquadId`, `s`.`Id`, `s1`.`SquadId` +"""); + } + + public override async Task Where_subquery_equality_to_null_with_composite_key_should_match_nulls(bool async) + { + await base.Where_subquery_equality_to_null_with_composite_key_should_match_nulls(async); + + AssertSql( +""" +SELECT `s`.`Id`, `s`.`Banner`, `s`.`Banner5`, `s`.`InternalNumber`, `s`.`Name` +FROM `Squads` AS `s` +WHERE NOT EXISTS ( + SELECT 1 + FROM ( + SELECT `g`.`Nickname`, `g`.`SquadId`, `g`.`AssignedCityName`, `g`.`CityOfBirthName`, `g`.`FullName`, `g`.`HasSoulPatch`, `g`.`LeaderNickname`, `g`.`LeaderSquadId`, `g`.`Rank`, 'Gear' AS `Discriminator` + FROM `Gears` AS `g` + UNION ALL + SELECT `o`.`Nickname`, `o`.`SquadId`, `o`.`AssignedCityName`, `o`.`CityOfBirthName`, `o`.`FullName`, `o`.`HasSoulPatch`, `o`.`LeaderNickname`, `o`.`LeaderSquadId`, `o`.`Rank`, 'Officer' AS `Discriminator` + FROM `Officers` AS `o` + ) AS `t` + WHERE (`s`.`Id` = `t`.`SquadId`) AND (`t`.`FullName` = 'Anthony Carmine')) +"""); + } + + public override async Task Where_subquery_equality_to_null_without_composite_key_should_match_null(bool async) + { + await base.Where_subquery_equality_to_null_without_composite_key_should_match_null(async); + + AssertSql( +""" +SELECT `t`.`Nickname`, `t`.`SquadId`, `t`.`AssignedCityName`, `t`.`CityOfBirthName`, `t`.`FullName`, `t`.`HasSoulPatch`, `t`.`LeaderNickname`, `t`.`LeaderSquadId`, `t`.`Rank`, `t`.`Discriminator` +FROM ( + SELECT `g`.`Nickname`, `g`.`SquadId`, `g`.`AssignedCityName`, `g`.`CityOfBirthName`, `g`.`FullName`, `g`.`HasSoulPatch`, `g`.`LeaderNickname`, `g`.`LeaderSquadId`, `g`.`Rank`, 'Gear' AS `Discriminator` + FROM `Gears` AS `g` + UNION ALL + SELECT `o`.`Nickname`, `o`.`SquadId`, `o`.`AssignedCityName`, `o`.`CityOfBirthName`, `o`.`FullName`, `o`.`HasSoulPatch`, `o`.`LeaderNickname`, `o`.`LeaderSquadId`, `o`.`Rank`, 'Officer' AS `Discriminator` + FROM `Officers` AS `o` +) AS `t` +WHERE NOT EXISTS ( + SELECT 1 + FROM `Weapons` AS `w` + WHERE (`t`.`FullName` = `w`.`OwnerFullName`) AND (`w`.`Name` = 'Hammer of Dawn')) +"""); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/TPCInheritanceQuerySingleStoreFixture.cs b/test/EFCore.SingleStore.FunctionalTests/Query/TPCInheritanceQuerySingleStoreFixture.cs index 4983438f2..222b066f4 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/TPCInheritanceQuerySingleStoreFixture.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/TPCInheritanceQuerySingleStoreFixture.cs @@ -13,7 +13,7 @@ protected override ITestStoreFactory TestStoreFactory => SingleStoreTestStoreFactory.Instance; // TODO: Add sequence support for server implementations that have them. - protected override bool UseGeneratedKeys + public override bool UseGeneratedKeys => false; protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/TPCInheritanceQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/TPCInheritanceQuerySingleStoreTest.cs index edbfef056..47c723aa9 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/TPCInheritanceQuerySingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/TPCInheritanceQuerySingleStoreTest.cs @@ -9,8 +9,10 @@ namespace EntityFrameworkCore.SingleStore.FunctionalTests.Query; public class TPCInheritanceQuerySingleStoreTest : TPCInheritanceQueryTestBase { - public TPCInheritanceQuerySingleStoreTest(TPCInheritanceQuerySingleStoreFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) + public TPCInheritanceQuerySingleStoreTest( + TPCInheritanceQuerySingleStoreFixture fixture, + ITestOutputHelper testOutputHelper) + : base(fixture, testOutputHelper) { Fixture.TestSqlLoggerFactory.Clear(); //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/TPCManyToManyNoTrackingQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/TPCManyToManyNoTrackingQuerySingleStoreTest.cs index 8e25135ef..ac838e68b 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/TPCManyToManyNoTrackingQuerySingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/TPCManyToManyNoTrackingQuerySingleStoreTest.cs @@ -35,7 +35,7 @@ WHERE NOT EXISTS ( SELECT 1 FROM `JoinOneToTwo` AS `j` INNER JOIN `EntityTwos` AS `e0` ON `j`.`TwoId` = `e0`.`Id` - WHERE (`e`.`Id` = `j`.`OneId`) AND NOT (`e0`.`Name` LIKE '%B%')) + WHERE (`e`.`Id` = `j`.`OneId`) AND (`e0`.`Name` NOT LIKE '%B%' OR (`e0`.`Name` IS NULL))) """); } @@ -122,7 +122,7 @@ UNION ALL SELECT `l`.`Id`, `l`.`Name`, `l`.`Number`, `l`.`IsGreen`, 'EntityLeaf' AS `Discriminator` FROM `Leaves` AS `l` ) AS `t` ON `j`.`EntityBranchId` = `t`.`Id` - WHERE (`e`.`Id` = `j`.`EntityOneId`) AND (`t`.`Name` IS NOT NULL AND (`t`.`Name` LIKE 'L%'))), `e`.`Id` + WHERE (`e`.`Id` = `j`.`EntityOneId`) AND (`t`.`Name` LIKE 'L%')), `e`.`Id` """); } @@ -155,7 +155,7 @@ ORDER BY ( SELECT COUNT(*) FROM `EntityTwoEntityTwo` AS `e0` INNER JOIN `EntityTwos` AS `e1` ON `e0`.`SelfSkipSharedLeftId` = `e1`.`Id` - WHERE (`e`.`Id` = `e0`.`SelfSkipSharedRightId`) AND (`e1`.`Name` IS NOT NULL AND (`e1`.`Name` LIKE 'L%'))) DESC, `e`.`Id` + WHERE (`e`.`Id` = `e0`.`SelfSkipSharedRightId`) AND (`e1`.`Name` LIKE 'L%')) DESC, `e`.`Id` """); } @@ -444,7 +444,7 @@ UNION ALL """); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override async Task Join_with_skip_navigation(bool async) { await base.Join_with_skip_navigation(async); @@ -463,7 +463,7 @@ ORDER BY `e2`.`Id` """); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override async Task Left_join_with_skip_navigation(bool async) { await base.Left_join_with_skip_navigation(async); @@ -2148,7 +2148,7 @@ WHERE NOT EXISTS ( SELECT 1 FROM `UnidirectionalJoinOneToTwo` AS `u0` INNER JOIN `UnidirectionalEntityTwos` AS `u1` ON `u0`.`TwoId` = `u1`.`Id` - WHERE (`u`.`Id` = `u0`.`OneId`) AND NOT (`u1`.`Name` LIKE '%B%')) + WHERE (`u`.`Id` = `u0`.`OneId`) AND (`u1`.`Name` NOT LIKE '%B%' OR (`u1`.`Name` IS NULL))) """); } @@ -2219,7 +2219,7 @@ UNION ALL SELECT `u2`.`Id`, `u2`.`Name`, `u2`.`Number`, `u2`.`IsGreen`, 'UnidirectionalEntityLeaf' AS `Discriminator` FROM `UnidirectionalLeaves` AS `u2` ) AS `t` ON `u0`.`UnidirectionalEntityBranchId` = `t`.`Id` - WHERE (`u`.`Id` = `u0`.`UnidirectionalEntityOneId`) AND (`t`.`Name` IS NOT NULL AND (`t`.`Name` LIKE 'L%'))), `u`.`Id` + WHERE (`u`.`Id` = `u0`.`UnidirectionalEntityOneId`) AND (`t`.`Name` LIKE 'L%')), `u`.`Id` """); } @@ -2285,7 +2285,7 @@ UNION ALL """); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override async Task Join_with_skip_navigation_unidirectional(bool async) { await base.Join_with_skip_navigation_unidirectional(async); diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/TPCManyToManyQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/TPCManyToManyQuerySingleStoreTest.cs index 9e90d1b4f..621dc84e7 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/TPCManyToManyQuerySingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/TPCManyToManyQuerySingleStoreTest.cs @@ -35,7 +35,7 @@ WHERE NOT EXISTS ( SELECT 1 FROM `JoinOneToTwo` AS `j` INNER JOIN `EntityTwos` AS `e0` ON `j`.`TwoId` = `e0`.`Id` - WHERE (`e`.`Id` = `j`.`OneId`) AND NOT (`e0`.`Name` LIKE '%B%')) + WHERE (`e`.`Id` = `j`.`OneId`) AND (`e0`.`Name` NOT LIKE '%B%' OR (`e0`.`Name` IS NULL))) """); } @@ -122,7 +122,7 @@ UNION ALL SELECT `l`.`Id`, `l`.`Name`, `l`.`Number`, `l`.`IsGreen`, 'EntityLeaf' AS `Discriminator` FROM `Leaves` AS `l` ) AS `t` ON `j`.`EntityBranchId` = `t`.`Id` - WHERE (`e`.`Id` = `j`.`EntityOneId`) AND (`t`.`Name` IS NOT NULL AND (`t`.`Name` LIKE 'L%'))), `e`.`Id` + WHERE (`e`.`Id` = `j`.`EntityOneId`) AND (`t`.`Name` LIKE 'L%')), `e`.`Id` """); } @@ -155,7 +155,7 @@ ORDER BY ( SELECT COUNT(*) FROM `EntityTwoEntityTwo` AS `e0` INNER JOIN `EntityTwos` AS `e1` ON `e0`.`SelfSkipSharedLeftId` = `e1`.`Id` - WHERE (`e`.`Id` = `e0`.`SelfSkipSharedRightId`) AND (`e1`.`Name` IS NOT NULL AND (`e1`.`Name` LIKE 'L%'))) DESC, `e`.`Id` + WHERE (`e`.`Id` = `e0`.`SelfSkipSharedRightId`) AND (`e1`.`Name` LIKE 'L%')) DESC, `e`.`Id` """); } @@ -2149,7 +2149,7 @@ WHERE NOT EXISTS ( SELECT 1 FROM `UnidirectionalJoinOneToTwo` AS `u0` INNER JOIN `UnidirectionalEntityTwos` AS `u1` ON `u0`.`TwoId` = `u1`.`Id` - WHERE (`u`.`Id` = `u0`.`OneId`) AND NOT (`u1`.`Name` LIKE '%B%')) + WHERE (`u`.`Id` = `u0`.`OneId`) AND (`u1`.`Name` NOT LIKE '%B%' OR (`u1`.`Name` IS NULL))) """); } @@ -2220,7 +2220,7 @@ UNION ALL SELECT `u2`.`Id`, `u2`.`Name`, `u2`.`Number`, `u2`.`IsGreen`, 'UnidirectionalEntityLeaf' AS `Discriminator` FROM `UnidirectionalLeaves` AS `u2` ) AS `t` ON `u0`.`UnidirectionalEntityBranchId` = `t`.`Id` - WHERE (`u`.`Id` = `u0`.`UnidirectionalEntityOneId`) AND (`t`.`Name` IS NOT NULL AND (`t`.`Name` LIKE 'L%'))), `u`.`Id` + WHERE (`u`.`Id` = `u0`.`UnidirectionalEntityOneId`) AND (`t`.`Name` LIKE 'L%')), `u`.`Id` """); } diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/TPHInheritanceQuerySingleStoreFixture.cs b/test/EFCore.SingleStore.FunctionalTests/Query/TPHInheritanceQuerySingleStoreFixture.cs new file mode 100644 index 000000000..6c1316a6a --- /dev/null +++ b/test/EFCore.SingleStore.FunctionalTests/Query/TPHInheritanceQuerySingleStoreFixture.cs @@ -0,0 +1,10 @@ +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.TestUtilities; +using EntityFrameworkCore.SingleStore.FunctionalTests.TestUtilities; + +namespace EntityFrameworkCore.SingleStore.FunctionalTests.Query; + +public class TPHInheritanceQuerySingleStoreFixture : TPHInheritanceQueryFixture +{ + protected override ITestStoreFactory TestStoreFactory => SingleStoreTestStoreFactory.Instance; +} diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/TPHInheritanceQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/TPHInheritanceQuerySingleStoreTest.cs new file mode 100644 index 000000000..245d9bb5d --- /dev/null +++ b/test/EFCore.SingleStore.FunctionalTests/Query/TPHInheritanceQuerySingleStoreTest.cs @@ -0,0 +1,12 @@ +using Microsoft.EntityFrameworkCore.Query; +using Xunit.Abstractions; + +namespace EntityFrameworkCore.SingleStore.FunctionalTests.Query; + +public class TPHInheritanceQuerySingleStoreTest : TPHInheritanceQueryTestBase +{ + public TPHInheritanceQuerySingleStoreTest(TPHInheritanceQuerySingleStoreFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture, testOutputHelper) + { + } +} diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/TPTFiltersInheritanceQuerySingleStoreFixture.cs b/test/EFCore.SingleStore.FunctionalTests/Query/TPTFiltersInheritanceQuerySingleStoreFixture.cs index ac1b822e8..c3631efd1 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/TPTFiltersInheritanceQuerySingleStoreFixture.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/TPTFiltersInheritanceQuerySingleStoreFixture.cs @@ -5,7 +5,7 @@ namespace EntityFrameworkCore.SingleStore.FunctionalTests.Query { public class TPTFiltersInheritanceQuerySingleStoreFixture : TPTInheritanceQuerySingleStoreFixture { - protected override bool EnableFilters + public override bool EnableFilters => true; diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/TPTGearsOfWarQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/TPTGearsOfWarQuerySingleStoreTest.cs index 9ddd5dfcf..844134393 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/TPTGearsOfWarQuerySingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/TPTGearsOfWarQuerySingleStoreTest.cs @@ -234,7 +234,7 @@ public override Task Subquery_projecting_non_nullable_scalar_contains_non_nullab return base.Subquery_projecting_non_nullable_scalar_contains_non_nullable_value_doesnt_need_null_expansion(async); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override Task Conditional_expression_with_test_being_simplified_to_constant_complex(bool isAsync) { return base.Conditional_expression_with_test_being_simplified_to_constant_complex(isAsync); @@ -252,19 +252,19 @@ public override Task Correlated_collection_with_very_complex_order_by(bool async return base.Correlated_collection_with_very_complex_order_by(async); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override Task Correlated_collections_with_FirstOrDefault(bool async) { return base.Correlated_collections_with_FirstOrDefault(async); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override Task FirstOrDefault_on_empty_collection_of_DateTime_in_subquery(bool async) { return base.FirstOrDefault_on_empty_collection_of_DateTime_in_subquery(async); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override Task FirstOrDefault_over_int_compared_to_zero(bool async) { return base.FirstOrDefault_over_int_compared_to_zero(async); @@ -294,13 +294,13 @@ public override Task Include_with_complex_order_by(bool async) return base.Include_with_complex_order_by(async); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override Task Multiple_orderby_with_navigation_expansion_on_one_of_the_order_bys_inside_subquery_complex_orderings(bool async) { return base.Multiple_orderby_with_navigation_expansion_on_one_of_the_order_bys_inside_subquery_complex_orderings(async); } - [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override Task Optional_navigation_with_collection_composite_key(bool async) { return base.Optional_navigation_with_collection_composite_key(async); @@ -445,6 +445,84 @@ public override Task Correlated_collection_with_groupby_with_complex_grouping_ke return base.Correlated_collection_with_groupby_with_complex_grouping_key_not_projecting_identifier_column_with_group_aggregate_in_final_projection(async); } + [ConditionalTheory(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed.")] + public override Task Where_subquery_with_ElementAtOrDefault_equality_to_null_with_composite_key(bool async) + { + return base.Where_subquery_with_ElementAtOrDefault_equality_to_null_with_composite_key(async); + } + + public override async Task DateTimeOffset_to_unix_time_milliseconds(bool async) + { + await base.DateTimeOffset_to_unix_time_milliseconds(async); + + AssertSql( +""" +@__unixEpochMilliseconds_0='0' + +SELECT `g`.`Nickname`, `g`.`SquadId`, `g`.`AssignedCityName`, `g`.`CityOfBirthName`, `g`.`FullName`, `g`.`HasSoulPatch`, `g`.`LeaderNickname`, `g`.`LeaderSquadId`, `g`.`Rank`, CASE + WHEN `o`.`Nickname` IS NOT NULL THEN 'Officer' +END AS `Discriminator`, `s`.`Id`, `s`.`Banner`, `s`.`Banner5`, `s`.`InternalNumber`, `s`.`Name`, `s1`.`SquadId`, `s1`.`MissionId` +FROM `Gears` AS `g` +LEFT JOIN `Officers` AS `o` ON (`g`.`Nickname` = `o`.`Nickname`) AND (`g`.`SquadId` = `o`.`SquadId`) +INNER JOIN `Squads` AS `s` ON `g`.`SquadId` = `s`.`Id` +LEFT JOIN `SquadMissions` AS `s1` ON `s`.`Id` = `s1`.`SquadId` +WHERE NOT EXISTS ( + SELECT 1 + FROM `SquadMissions` AS `s0` + INNER JOIN `Missions` AS `m` ON `s0`.`MissionId` = `m`.`Id` + WHERE (`s`.`Id` = `s0`.`SquadId`) AND (@__unixEpochMilliseconds_0 = (TIMESTAMPDIFF(microsecond, '1970-01-01 00:00:00', `m`.`Timeline`)) DIV (1000))) +ORDER BY `g`.`Nickname`, `g`.`SquadId`, `s`.`Id`, `s1`.`SquadId` +"""); + } + + public override async Task DateTimeOffset_to_unix_time_seconds(bool async) + { + await base.DateTimeOffset_to_unix_time_seconds(async); + + AssertSql( +""" +@__unixEpochSeconds_0='0' + +SELECT `g`.`Nickname`, `g`.`SquadId`, `g`.`AssignedCityName`, `g`.`CityOfBirthName`, `g`.`FullName`, `g`.`HasSoulPatch`, `g`.`LeaderNickname`, `g`.`LeaderSquadId`, `g`.`Rank`, CASE + WHEN `o`.`Nickname` IS NOT NULL THEN 'Officer' +END AS `Discriminator`, `s`.`Id`, `s`.`Banner`, `s`.`Banner5`, `s`.`InternalNumber`, `s`.`Name`, `s1`.`SquadId`, `s1`.`MissionId` +FROM `Gears` AS `g` +LEFT JOIN `Officers` AS `o` ON (`g`.`Nickname` = `o`.`Nickname`) AND (`g`.`SquadId` = `o`.`SquadId`) +INNER JOIN `Squads` AS `s` ON `g`.`SquadId` = `s`.`Id` +LEFT JOIN `SquadMissions` AS `s1` ON `s`.`Id` = `s1`.`SquadId` +WHERE NOT EXISTS ( + SELECT 1 + FROM `SquadMissions` AS `s0` + INNER JOIN `Missions` AS `m` ON `s0`.`MissionId` = `m`.`Id` + WHERE (`s`.`Id` = `s0`.`SquadId`) AND (@__unixEpochSeconds_0 = TIMESTAMPDIFF(second, '1970-01-01 00:00:00', `m`.`Timeline`))) +ORDER BY `g`.`Nickname`, `g`.`SquadId`, `s`.`Id`, `s1`.`SquadId` +"""); + } + + [SupportedServerVersionCondition(nameof(ServerVersionSupport.LimitWithNonConstantValue))] + public override async Task Where_subquery_with_ElementAt_using_column_as_index(bool async) + { + await base.Where_subquery_with_ElementAt_using_column_as_index(async); + + AssertSql(""); + } + + public override async Task Where_datetimeoffset_hour_component(bool async) + { + await AssertQuery( + async, + ss => from m in ss.Set() + where m.Timeline.Hour == /* 10 */ 8 + select m); + + AssertSql( + """ + SELECT `m`.`Id`, `m`.`CodeName`, `m`.`Date`, `m`.`Duration`, `m`.`Rating`, `m`.`Time`, `m`.`Timeline` + FROM `Missions` AS `m` + WHERE EXTRACT(hour FROM `m`.`Timeline`) = 8 + """); + } + private string AssertSql(string expected) { Fixture.TestSqlLoggerFactory.AssertBaseline(new[] {expected}); diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/TPTInheritanceQuerySingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/TPTInheritanceQuerySingleStoreTest.cs index 08f52a5d8..ce8b3bc30 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/TPTInheritanceQuerySingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/TPTInheritanceQuerySingleStoreTest.cs @@ -11,14 +11,16 @@ namespace EntityFrameworkCore.SingleStore.FunctionalTests.Query { public class TPTInheritanceQuerySingleStoreTest : TPTInheritanceQueryTestBase { - public TPTInheritanceQuerySingleStoreTest(TPTInheritanceQuerySingleStoreFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) + public TPTInheritanceQuerySingleStoreTest( + TPTInheritanceQuerySingleStoreFixture fixture, + ITestOutputHelper testOutputHelper) + : base(fixture, testOutputHelper) { Fixture.TestSqlLoggerFactory.Clear(); //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } - [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore.")] + [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore Distributed.")] public override void Setting_foreign_key_to_a_different_type_throws() { base.Setting_foreign_key_to_a_different_type_throws(); diff --git a/test/EFCore.SingleStore.FunctionalTests/Query/WarningsSingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/Query/WarningsSingleStoreTest.cs index fbe6f18cb..0be87d577 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Query/WarningsSingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Query/WarningsSingleStoreTest.cs @@ -10,13 +10,13 @@ public WarningsSingleStoreTest(QueryNoClientEvalSingleStoreFixture fixture) { } - [ConditionalFact(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalFact(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override void FirstOrDefault_without_orderby_and_filter_issues_warning_subquery() { base.FirstOrDefault_without_orderby_and_filter_issues_warning_subquery(); } - [ConditionalFact(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore")] + [ConditionalFact(Skip = "Feature 'Correlated subselect that can not be transformed and does not match on shard keys' is not supported by SingleStore Distributed")] public override void LastOrDefault_with_order_by_does_not_issue_client_eval_warning() { base.LastOrDefault_with_order_by_does_not_issue_client_eval_warning(); diff --git a/test/EFCore.SingleStore.FunctionalTests/SingleStoreApiConsistencyTest.cs b/test/EFCore.SingleStore.FunctionalTests/SingleStoreApiConsistencyTest.cs index 41e439508..f362c1d65 100644 --- a/test/EFCore.SingleStore.FunctionalTests/SingleStoreApiConsistencyTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/SingleStoreApiConsistencyTest.cs @@ -38,46 +38,54 @@ public class SingleStoreApiConsistencyFixture : ApiConsistencyFixtureBase }; public override - List<(Type Type, - Type ReadonlyExtensions, + Dictionary MetadataExtensionTypes { get; } = new() { - ( + { typeof(IReadOnlyModel), - typeof(SingleStoreModelExtensions), - typeof(SingleStoreModelExtensions), - typeof(SingleStoreModelExtensions), - typeof(SingleStoreModelBuilderExtensions), - null - ), - ( + ( + typeof(SingleStoreModelExtensions), + typeof(SingleStoreModelExtensions), + typeof(SingleStoreModelExtensions), + typeof(SingleStoreModelBuilderExtensions), + null + ) + }, + { typeof(IReadOnlyEntityType), - typeof(SingleStoreEntityTypeExtensions), - typeof(SingleStoreEntityTypeExtensions), - typeof(SingleStoreEntityTypeExtensions), - typeof(SingleStoreEntityTypeBuilderExtensions), - null - ), - ( + ( + typeof(SingleStoreEntityTypeExtensions), + typeof(SingleStoreEntityTypeExtensions), + typeof(SingleStoreEntityTypeExtensions), + typeof(SingleStoreEntityTypeBuilderExtensions), + null + ) + }, + { typeof(IReadOnlyProperty), - typeof(SingleStorePropertyExtensions), - typeof(SingleStorePropertyExtensions), - typeof(SingleStorePropertyExtensions), - typeof(SingleStorePropertyBuilderExtensions), - null - ), - ( + ( + typeof(SingleStorePropertyExtensions), + typeof(SingleStorePropertyExtensions), + typeof(SingleStorePropertyExtensions), + typeof(SingleStorePropertyBuilderExtensions), + null + ) + }, + { typeof(IReadOnlyIndex), - typeof(SingleStoreIndexExtensions), - typeof(SingleStoreIndexExtensions), - typeof(SingleStoreIndexExtensions), - typeof(SingleStoreIndexBuilderExtensions), - null - ) + ( + typeof(SingleStoreIndexExtensions), + typeof(SingleStoreIndexExtensions), + typeof(SingleStoreIndexExtensions), + typeof(SingleStoreIndexBuilderExtensions), + null + ) + }, }; public override HashSet UnmatchedMetadataMethods { get; } = new() diff --git a/test/EFCore.SingleStore.FunctionalTests/SingleStoreComplianceTest.cs b/test/EFCore.SingleStore.FunctionalTests/SingleStoreComplianceTest.cs index 426128faa..fe85f2fe6 100644 --- a/test/EFCore.SingleStore.FunctionalTests/SingleStoreComplianceTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/SingleStoreComplianceTest.cs @@ -24,9 +24,12 @@ public class SingleStoreComplianceTest : RelationalComplianceTestBase typeof(LoggingRelationalTestBase<,>), // We have our own JSON support for now - typeof(JsonUpdateTestBase<>), - typeof(JsonQueryTestBase<>), typeof(JsonQueryAdHocTestBase), + typeof(JsonQueryTestBase<>), + typeof(JsonTypesRelationalTestBase), + typeof(JsonTypesTestBase), + typeof(JsonUpdateTestBase<>), + typeof(OptionalDependentQueryTestBase<>), typeof(FieldMappingTestBase<>), typeof(GraphUpdatesTestBase<>), diff --git a/test/EFCore.SingleStore.FunctionalTests/SingleStoreConnectionStringOptionsValidatorTests.cs b/test/EFCore.SingleStore.FunctionalTests/SingleStoreConnectionStringOptionsValidatorTests.cs new file mode 100644 index 000000000..53a832d0e --- /dev/null +++ b/test/EFCore.SingleStore.FunctionalTests/SingleStoreConnectionStringOptionsValidatorTests.cs @@ -0,0 +1,103 @@ +using System; +using System.Linq; +using System.Reflection; +using Xunit; +using SingleStoreConnector; +using EntityFrameworkCore.SingleStore.Storage.Internal; +using EntityFrameworkCore.SingleStore.Tests; + +namespace EntityFrameworkCore.SingleStore.FunctionalTests +{ + public class SingleStoreConnectionStringOptionsValidatorTests + { + private readonly SingleStoreConnectionStringOptionsValidator _validator + = new(); + + [Fact] + public void EnsureMandatoryOptions_AppendsConnectionAttributesWithoutRemovingExisting() + { + var attrs = "foo:bar,baz:qux"; + var cs = AppConfig.ConnectionString + ";" + $"ConnectionAttributes={attrs}"; + + var changed = _validator.EnsureMandatoryOptions(ref cs); + + Assert.True(changed); + var newCsb = new SingleStoreConnectionStringBuilder(cs); + var parts = newCsb.ConnectionAttributes + .TrimEnd(',') + .Split(',', StringSplitOptions.RemoveEmptyEntries) + .Select(p => p.Trim()) + .ToArray(); + + Assert.Contains("foo:bar", parts); + Assert.Contains("baz:qux", parts); + var programVersion = Assembly.GetExecutingAssembly().GetName().Version; + Assert.Contains(parts, p => p.StartsWith("_connector_name:SingleStore Entity Framework Core provider")); + Assert.Contains(parts, p => p.StartsWith($"_connector_version:{programVersion}")); + } + + [Fact] + public void EnsureMandatoryOptions_DoesNotDuplicateConnectionAttributes_OnRepeatedCalls() + { + var cs = AppConfig.ConnectionString; + + Assert.True(_validator.EnsureMandatoryOptions(ref cs)); + Assert.False(_validator.EnsureMandatoryOptions(ref cs)); + + var programVersion = Assembly.GetExecutingAssembly().GetName().Version; + var parts = new SingleStoreConnectionStringBuilder(cs) + .ConnectionAttributes + .TrimEnd(',') + .Split(',', StringSplitOptions.RemoveEmptyEntries) + .Select(p => p.Trim()) + .ToArray(); + + Assert.Equal(1, parts.Count(p => p.StartsWith("_connector_name:SingleStore Entity Framework Core provider"))); + Assert.Equal(1, parts.Count(p => p.StartsWith($"_connector_version:{programVersion}"))); + } + + [Fact] + public void EnsureMandatoryOptions_SetsMandatoryFlags_WhenMissing() + { + var builder = new SingleStoreConnectionStringBuilder(AppConfig.ConnectionString) + { + AllowUserVariables = false, + UseAffectedRows = true + }; + var cs = builder.ConnectionString; + + var changed = _validator.EnsureMandatoryOptions(ref cs); + + Assert.True(changed); + var result = new SingleStoreConnectionStringBuilder(cs); + Assert.True(result.AllowUserVariables); + Assert.False(result.UseAffectedRows); + } + + [Fact] + public void EnsureMandatoryOptions_DbConnection_InjectsAttributesAndFlags() + { + using var conn = new SingleStoreConnection(AppConfig.ConnectionString); + + var changed = _validator.EnsureMandatoryOptions(conn); + + Assert.True(changed); + var result = new SingleStoreConnectionStringBuilder(conn.ConnectionString); + Assert.True(result.AllowUserVariables); + Assert.False(result.UseAffectedRows); + Assert.Contains("_connector_name:SingleStore Entity Framework Core provider", result.ConnectionAttributes); + var programVersion = Assembly.GetExecutingAssembly().GetName().Version; + Assert.Contains($"_connector_version:{programVersion}", result.ConnectionAttributes); + } + + [Fact] + public void EnsureMandatoryOptions_DbDataSource_ThrowsWhenMissingMandatoryOptions() + { + var dataSource = new SingleStoreDataSourceBuilder(AppConfig.ConnectionString).Build(); + + var ex = Assert.Throws( + () => _validator.EnsureMandatoryOptions(dataSource)); + Assert.Contains("AllowUserVariables=True;UseAffectedRows=False", ex.Message); + } + } +} diff --git a/test/EFCore.SingleStore.FunctionalTests/SingleStoreMigrationsSqlGeneratorTest.cs b/test/EFCore.SingleStore.FunctionalTests/SingleStoreMigrationsSqlGeneratorTest.cs index 2f73c6bf6..a6c9bf6b1 100644 --- a/test/EFCore.SingleStore.FunctionalTests/SingleStoreMigrationsSqlGeneratorTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/SingleStoreMigrationsSqlGeneratorTest.cs @@ -85,7 +85,7 @@ public override void AddColumnOperation_with_precision_and_scale_no_model() @"ALTER TABLE `Person` ADD `Pi` decimal(20,7) NOT NULL;"); } - [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore.")] + [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore Distributed.")] public override void AddForeignKeyOperation_without_principal_columns() { base.AddForeignKeyOperation_without_principal_columns(); @@ -583,7 +583,7 @@ public virtual void CreateDatabaseOperation_with_collation() Sql); } - [ConditionalFact(Skip = "Feature 'ALTER DATABASE CHARACTER SET' is not supported by SingleStore.")] + [ConditionalFact(Skip = "Feature 'ALTER DATABASE CHARACTER SET' is not supported by SingleStore Distributed.")] public virtual void AlterDatabaseOperation_with_charset() { Generate( @@ -597,7 +597,7 @@ public virtual void AlterDatabaseOperation_with_charset() Sql); } - [ConditionalFact(Skip = "Feature 'ALTER DATABASE CHARACTER SET' is not supported by SingleStore.")] + [ConditionalFact(Skip = "Feature 'ALTER DATABASE CHARACTER SET' is not supported by SingleStore Distributed.")] public virtual void AlterDatabaseOperation_with_collation() { Generate( @@ -1001,7 +1001,7 @@ public void AlterColumnOperation_ComputedColumnSql_stored() Sql); } - [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore.")] + [ConditionalFact(Skip = "Feature 'FOREIGN KEY' is not supported by SingleStore Distributed.")] public virtual void AddForeignKeyOperation_with_long_name() { Generate( @@ -1311,7 +1311,7 @@ PRIMARY KEY (`Name`, `Brand`(20)) ignoreLineEndingDifferences: true); } - [ConditionalFact(Skip = "Feature 'ALTER TABLE COLLATE' is not supported by SingleStore.")] + [ConditionalFact(Skip = "Feature 'ALTER TABLE COLLATE' is not supported by SingleStore Distributed.")] public virtual void AlterTableOperation_with_collation() { Generate( @@ -1346,7 +1346,7 @@ public virtual void AlterTableOperation_with_collation() ignoreLineEndingDifferences: true); } - [ConditionalFact(Skip = "Feature 'ALTER TABLE COLLATE' is not supported by SingleStore.")] + [ConditionalFact(Skip = "Feature 'ALTER TABLE COLLATE' is not supported by SingleStore Distributed.")] public virtual void AlterTableOperation_with_collation_reset() { Generate( @@ -1430,7 +1430,7 @@ PRIMARY KEY (`Name`, `Brand`(20)) ignoreLineEndingDifferences: true); } - [ConditionalFact(Skip = "Feature 'ALTER TABLE CHARACTER SET' is not supported by SingleStore.")] + [ConditionalFact(Skip = "Feature 'ALTER TABLE CHARACTER SET' is not supported by SingleStore Distributed.")] public virtual void AlterTableOperation_with_charset() { Generate( @@ -1465,7 +1465,7 @@ public virtual void AlterTableOperation_with_charset() ignoreLineEndingDifferences: true); } - [ConditionalFact(Skip = "Feature 'ALTER TABLE CHARACTER SET' is not supported by SingleStore.")] + [ConditionalFact(Skip = "Feature 'ALTER TABLE CHARACTER SET' is not supported by SingleStore Distributed.")] public virtual void AlterTableOperation_with_charset_reset() { Generate( @@ -1612,6 +1612,17 @@ PRIMARY KEY (`Name`, `Brand`(20)) ignoreLineEndingDifferences: true); } + [SupportedServerVersionCondition(nameof(ServerVersionSupport.Sequences))] + public override void Sequence_restart_operation(long? startsAt) + { + base.Sequence_restart_operation(startsAt); + + Assert.Equal( + $@"ALTER SEQUENCE `TestRestartSequenceOperation` {(startsAt > 0 ? $"START WITH {startsAt} RESTART" : "RESTART")};" + EOL, + Sql, + ignoreLineEndingDifferences: true); + } + [ConditionalFact] [SupportedServerVersionCondition(nameof(ServerVersionSupport.Sequences))] public virtual void AlterSequenceOperation_with_minValue_and_maxValue() diff --git a/test/EFCore.SingleStore.FunctionalTests/Storage/SingleStoreJsonMicrosoftTypeMappingTest.cs b/test/EFCore.SingleStore.FunctionalTests/Storage/SingleStoreJsonMicrosoftTypeMappingTest.cs index 232286dbc..228274148 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Storage/SingleStoreJsonMicrosoftTypeMappingTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Storage/SingleStoreJsonMicrosoftTypeMappingTest.cs @@ -2,6 +2,7 @@ using System.Text.Json; using Microsoft.EntityFrameworkCore.Design.Internal; using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Json; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using EntityFrameworkCore.SingleStore.Internal; using EntityFrameworkCore.SingleStore.Json.Microsoft.Storage.Internal; @@ -100,6 +101,7 @@ public class Order private static readonly SingleStoreTypeMappingSource Mapper = new SingleStoreTypeMappingSource( new TypeMappingSourceDependencies( new ValueConverterSelector(new ValueConverterSelectorDependencies()), + new JsonValueReaderWriterSource(new JsonValueReaderWriterSourceDependencies()), Array.Empty()), new RelationalTypeMappingSourceDependencies( new [] {new SingleStoreJsonMicrosoftTypeMappingSourcePlugin(new SingleStoreOptions())}), diff --git a/test/EFCore.SingleStore.FunctionalTests/Storage/SingleStoreJsonNewtonsoftTypeMappingTest.cs b/test/EFCore.SingleStore.FunctionalTests/Storage/SingleStoreJsonNewtonsoftTypeMappingTest.cs index 054f6dc56..00aff5b86 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Storage/SingleStoreJsonNewtonsoftTypeMappingTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Storage/SingleStoreJsonNewtonsoftTypeMappingTest.cs @@ -1,6 +1,7 @@ using System; using Microsoft.EntityFrameworkCore.Design.Internal; using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Json; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Newtonsoft.Json.Linq; using EntityFrameworkCore.SingleStore.Internal; @@ -86,6 +87,7 @@ public class Order private static readonly SingleStoreTypeMappingSource Mapper = new SingleStoreTypeMappingSource( new TypeMappingSourceDependencies( new ValueConverterSelector(new ValueConverterSelectorDependencies()), + new JsonValueReaderWriterSource(new JsonValueReaderWriterSourceDependencies()), Array.Empty()), new RelationalTypeMappingSourceDependencies( new [] {new SingleStoreJsonNewtonsoftTypeMappingSourcePlugin(new SingleStoreOptions())}), diff --git a/test/EFCore.SingleStore.FunctionalTests/Storage/SingleStoreTypeMappingTest.cs b/test/EFCore.SingleStore.FunctionalTests/Storage/SingleStoreTypeMappingTest.cs index 002d3ad7d..6289412a8 100644 --- a/test/EFCore.SingleStore.FunctionalTests/Storage/SingleStoreTypeMappingTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/Storage/SingleStoreTypeMappingTest.cs @@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore.Design.Internal; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Json; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using EntityFrameworkCore.SingleStore.Infrastructure; using EntityFrameworkCore.SingleStore.Internal; @@ -432,6 +433,7 @@ private static SingleStoreTypeMappingSource GetMapper(SingleStoreOptions options => new SingleStoreTypeMappingSource( new TypeMappingSourceDependencies( new ValueConverterSelector(new ValueConverterSelectorDependencies()), + new JsonValueReaderWriterSource(new JsonValueReaderWriterSourceDependencies()), Array.Empty()), new RelationalTypeMappingSourceDependencies( Array.Empty()), diff --git a/test/EFCore.SingleStore.FunctionalTests/StoreGeneratedSingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/StoreGeneratedSingleStoreTest.cs index ead472371..c014cbb3e 100644 --- a/test/EFCore.SingleStore.FunctionalTests/StoreGeneratedSingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/StoreGeneratedSingleStoreTest.cs @@ -20,6 +20,9 @@ protected override void UseTransaction(DatabaseFacade facade, IDbContextTransact public class StoreGeneratedSingleStoreFixture : StoreGeneratedSingleStoreFixtureBase { + protected override string StoreName + => "StoreGeneratedTest"; + protected override ITestStoreFactory TestStoreFactory => SingleStoreTestStoreFactory.Instance; public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) diff --git a/test/EFCore.SingleStore.FunctionalTests/TPTTableSplittingSingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/TPTTableSplittingSingleStoreTest.cs index 094127fef..641d2a4e4 100644 --- a/test/EFCore.SingleStore.FunctionalTests/TPTTableSplittingSingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/TPTTableSplittingSingleStoreTest.cs @@ -15,6 +15,12 @@ public TPTTableSplittingSingleStoreTest(ITestOutputHelper testOutputHelper) { } + public override Task Can_insert_dependent_with_just_one_parent() + { + // This scenario is not valid for TPT + return Task.CompletedTask; + } + protected override ITestStoreFactory TestStoreFactory => SingleStoreTestStoreFactory.Instance; diff --git a/test/EFCore.SingleStore.FunctionalTests/TestModels/Northwind/NorthwindSingleStoreContext.cs b/test/EFCore.SingleStore.FunctionalTests/TestModels/Northwind/NorthwindSingleStoreContext.cs new file mode 100644 index 000000000..94316b9e2 --- /dev/null +++ b/test/EFCore.SingleStore.FunctionalTests/TestModels/Northwind/NorthwindSingleStoreContext.cs @@ -0,0 +1,51 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.TestModels.Northwind; + +namespace EntityFrameworkCore.SingleStore.FunctionalTests.TestModels.Northwind; + +public class NorthwindSingleStoreContext : NorthwindRelationalContext +{ + public NorthwindSingleStoreContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity( + b => + { + b.Property(e => e.EmployeeID).HasColumnType("int"); + b.Property(e => e.ReportsTo).HasColumnType("int"); + }); + + modelBuilder.Entity( + b => + { + b.Property(e => e.CustomerID).IsFixedLength(); + }); + + modelBuilder.Entity( + b => + { + b.Property(e => e.CustomerID).IsFixedLength(); + b.Property(e => e.EmployeeID).HasColumnType("int"); + b.Property(o => o.OrderDate).HasColumnType("datetime"); + }); + + modelBuilder.Entity( + b => + { + b.Property(p => p.UnitsInStock).HasColumnType("smallint"); + }); + + modelBuilder.Entity( + b => + { + b.Property(p => p.Quantity).HasColumnType("smallint"); + b.Property(p => p.Discount).HasColumnType("float"); + }); + } +} diff --git a/test/EFCore.SingleStore.FunctionalTests/TestUtilities/SingleStoreTestHelpers.cs b/test/EFCore.SingleStore.FunctionalTests/TestUtilities/SingleStoreTestHelpers.cs index 4e346bb7a..d4241bd0a 100644 --- a/test/EFCore.SingleStore.FunctionalTests/TestUtilities/SingleStoreTestHelpers.cs +++ b/test/EFCore.SingleStore.FunctionalTests/TestUtilities/SingleStoreTestHelpers.cs @@ -7,6 +7,7 @@ using Microsoft.EntityFrameworkCore.TestUtilities; using Microsoft.Extensions.DependencyInjection; using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; +using EntityFrameworkCore.SingleStore.Infrastructure.Internal; using EntityFrameworkCore.SingleStore.Tests; //ReSharper disable once CheckNamespace @@ -116,5 +117,12 @@ public static string SingleStoreBug96947Workaround(string innerSql, string type => AppConfig.ServerVersion.Supports.SingleStoreBug96947Workaround ? $@"CAST({innerSql} AS {type})" : innerSql; + + public static bool HasPrimitiveCollectionsSupport(SharedStoreFixtureBase fixture) + where TContext : DbContext + { + return AppConfig.ServerVersion.Supports.JsonTable && + fixture.CreateOptions().GetExtension().PrimitiveCollectionsSupport; + } } } diff --git a/test/EFCore.SingleStore.FunctionalTests/TestUtilities/SingleStoreTestStore.cs b/test/EFCore.SingleStore.FunctionalTests/TestUtilities/SingleStoreTestStore.cs index 1a1591e19..d942ff57b 100644 --- a/test/EFCore.SingleStore.FunctionalTests/TestUtilities/SingleStoreTestStore.cs +++ b/test/EFCore.SingleStore.FunctionalTests/TestUtilities/SingleStoreTestStore.cs @@ -147,31 +147,32 @@ protected override void Initialize(Func createContext, Action clean) { + using var master = new SingleStoreConnection(CreateAdminConnectionString()); + master.Open(); + if (DatabaseExists(Name)) { - if (_scriptPath != null && !TestEnvironment.IsCI) + /*if (_scriptPath != null && !TestEnvironment.IsCI) { return false; - } + }*/ using (var context = new DbContext( - AddProviderOptions( - new DbContextOptionsBuilder() - .EnableServiceProviderCaching(false)) - .Options)) + AddProviderOptions( + new DbContextOptionsBuilder() + .EnableServiceProviderCaching(false)) + .Options)) { clean?.Invoke(context); Clean(context); - return true; } - } - using (var master = new SingleStoreConnection(CreateAdminConnectionString())) - { - master.Open(); - ExecuteNonQuery(master, GetCreateDatabaseStatement(Name, DatabaseCharSet, DatabaseCollation)); + ExecuteNonQuery(master, $"DROP DATABASE IF EXISTS `{Name}`;"); } + ExecuteNonQuery( + master, + GetCreateDatabaseStatement(Name, DatabaseCharSet, DatabaseCollation)); return true; } diff --git a/test/EFCore.SingleStore.FunctionalTests/TestUtilities/Xunit/SingleStoreXunitTestRunner.cs b/test/EFCore.SingleStore.FunctionalTests/TestUtilities/Xunit/SingleStoreXunitTestRunner.cs index 1cd9cda4e..08d7fb729 100644 --- a/test/EFCore.SingleStore.FunctionalTests/TestUtilities/Xunit/SingleStoreXunitTestRunner.cs +++ b/test/EFCore.SingleStore.FunctionalTests/TestUtilities/Xunit/SingleStoreXunitTestRunner.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; @@ -133,17 +134,30 @@ protected virtual bool SkipFailedTest(Exception exception) foreach (var innerException in aggregateException.InnerExceptions) { if (!skip || - innerException is not InvalidOperationException || - !innerException.Message.StartsWith("The LINQ expression '") || - !innerException.Message.Contains("' could not be translated.")) + innerException is not InvalidOperationException) { return false; } - skip &= !supports.OuterApply && innerException.Message.Contains("OUTER APPLY") || - !supports.CrossApply && innerException.Message.Contains("CROSS APPLY") || - !supports.WindowFunctions && innerException.Message.Contains("ROW_NUMBER() OVER") || - !supports.ExceptIntercept && (innerException.Message.Contains("EXCEPT") || innerException.Message.Contains("INTERSECT")); + if (innerException.Message.StartsWith("The LINQ expression '") || + innerException.Message.Contains("' could not be translated.")) + { + skip &= !supports.OuterApply && innerException.Message.Contains("OUTER APPLY") || + !supports.CrossApply && innerException.Message.Contains("CROSS APPLY") || + !supports.WindowFunctions && innerException.Message.Contains("ROW_NUMBER() OVER") || + !supports.ExceptIntercept && + (innerException.Message.Contains("EXCEPT") || innerException.Message.Contains("INTERSECT")) || + !supports.JsonTable && (innerException.Message.Contains("JSON_TABLE") || + innerException.Message.Contains("JsonTable")) || + innerException.Message.Contains("Primitive collections support has not been enabled.") || + innerException.Message.Contains("MySqlBipolarExpression"); + } + else + { + skip &= (!supports.JsonTable || + !supports.JsonTableImplementationWithAggregate) && (innerException.Message.Contains("JSON_TABLE") || + innerException.Message.Contains("JsonTable")); + } } return skip; diff --git a/test/EFCore.SingleStore.FunctionalTests/TransactionSingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/TransactionSingleStoreTest.cs index 33aad2b83..6c52f7b53 100644 --- a/test/EFCore.SingleStore.FunctionalTests/TransactionSingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/TransactionSingleStoreTest.cs @@ -25,31 +25,31 @@ public TransactionSingleStoreTest(TransactionSingleStoreFixture fixture) protected override bool SavepointsSupported => false; - [ConditionalTheory(Skip = "Feature 'SAVEPOINT' is not supported by SingleStore.")] + [ConditionalTheory(Skip = "Feature 'SAVEPOINT' is not supported by SingleStore Distributed.")] public override async Task Savepoint_can_be_released(bool async) { await base.Savepoint_can_be_released(async); } - [ConditionalTheory(Skip = "Feature 'SAVEPOINT' is not supported by SingleStore.")] + [ConditionalTheory(Skip = "Feature 'SAVEPOINT' is not supported by SingleStore Distributed.")] public override async Task Savepoint_can_be_rolled_back(bool async) { await base.Savepoint_can_be_rolled_back(async); } - [ConditionalTheory(Skip = "Feature 'SAVEPOINT' is not supported by SingleStore.")] + [ConditionalTheory(Skip = "Feature 'SAVEPOINT' is not supported by SingleStore Distributed.")] public override async Task Savepoint_name_is_quoted(bool async) { await base.Savepoint_name_is_quoted(async); } - [ConditionalFact(Skip = "Feature 'XaTransactions' is not supported by SingleStore.")] + [ConditionalFact(Skip = "Feature 'XaTransactions' is not supported by SingleStore Distributed.")] public override void BeginTransaction_throws_if_ambient_transaction_started() { base.BeginTransaction_throws_if_ambient_transaction_started(); } - [ConditionalFact(Skip = "Feature 'XaTransactions' is not supported by SingleStore.")] + [ConditionalFact(Skip = "Feature 'XaTransactions' is not supported by SingleStore Distributed.")] public override void EnlistTransaction_throws_if_ambient_transaction_started() { base.EnlistTransaction_throws_if_ambient_transaction_started(); diff --git a/test/EFCore.SingleStore.FunctionalTests/UpdatesSingleStoreTest.cs b/test/EFCore.SingleStore.FunctionalTests/UpdatesSingleStoreTest.cs index 7c50a3475..45927ab82 100644 --- a/test/EFCore.SingleStore.FunctionalTests/UpdatesSingleStoreTest.cs +++ b/test/EFCore.SingleStore.FunctionalTests/UpdatesSingleStoreTest.cs @@ -25,7 +25,7 @@ public UpdatesSingleStoreTest(UpdatesSingleStoreFixture fixture) fixture.TestSqlLoggerFactory.Clear(); } - [ConditionalFact(Skip = "Feature 'Adding an INDEX with multiple columns on a columnstore table where any column in the new index is already in an index' is not supported by SingleStore.")] + [ConditionalFact(Skip = "Feature 'Adding an INDEX with multiple columns on a columnstore table where any column in the new index is already in an index' is not supported by SingleStore Distributed.")] public override void Identifiers_are_generated_correctly() { using (var context = CreateContext()) diff --git a/test/EFCore.SingleStore.FunctionalTests/WithConstructorsSingleStoreTestBase.cs b/test/EFCore.SingleStore.FunctionalTests/WithConstructorsSingleStoreTestBase.cs index bbd3e057b..82ca8300c 100644 --- a/test/EFCore.SingleStore.FunctionalTests/WithConstructorsSingleStoreTestBase.cs +++ b/test/EFCore.SingleStore.FunctionalTests/WithConstructorsSingleStoreTestBase.cs @@ -496,8 +496,6 @@ public virtual void Detaching_entity_resets_lazy_loader_so_it_can_be_reattached( Assert.NotNull(post.GetLoader()); context.Entry(post).State = EntityState.Detached; - - Assert.Null(post.GetLoader()); } Assert.Null(post.LazyPropertyBlog); @@ -570,8 +568,6 @@ public virtual void Detaching_entity_resets_lazy_loader_field_so_it_can_be_reatt Assert.NotNull(post.GetLoader()); context.Entry(post).State = EntityState.Detached; - - Assert.Null(post.GetLoader()); } Assert.Null(post.LazyFieldBlog); @@ -1672,23 +1668,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con modelBuilder.Entity(); - // Manually configure service fields since there is no public API yet - - var bindingFactories = context.GetService(); - - var blogServiceProperty = modelBuilder.Entity().Metadata.AddServiceProperty( - typeof(LazyFieldBlog).GetRuntimeFields().Single(f => f.Name == "_loader")); - - blogServiceProperty.ParameterBinding = - (ServiceParameterBinding)bindingFactories.FindFactory(typeof(ILazyLoader), "_loader") - .Bind(blogServiceProperty.DeclaringEntityType, typeof(ILazyLoader), "_loader"); - - var postServiceProperty = modelBuilder.Entity().Metadata.AddServiceProperty( - typeof(LazyFieldPost).GetRuntimeFields().Single(f => f.Name == "_loader")); - - postServiceProperty.ParameterBinding = - (ServiceParameterBinding)bindingFactories.FindFactory(typeof(ILazyLoader), "_loader") - .Bind(postServiceProperty.DeclaringEntityType, typeof(ILazyLoader), "_loader"); + modelBuilder.Entity(); + modelBuilder.Entity(); } protected override void Seed(WithConstructorsContext context) diff --git a/test/EFCore.SingleStore.IntegrationTests/EFCore.SingleStore.IntegrationTests.csproj b/test/EFCore.SingleStore.IntegrationTests/EFCore.SingleStore.IntegrationTests.csproj index 8b0c301f9..23f822017 100644 --- a/test/EFCore.SingleStore.IntegrationTests/EFCore.SingleStore.IntegrationTests.csproj +++ b/test/EFCore.SingleStore.IntegrationTests/EFCore.SingleStore.IntegrationTests.csproj @@ -21,6 +21,10 @@ + + + + diff --git a/test/EFCore.SingleStore.IntegrationTests/Tests/Models/ExpressionTest.cs b/test/EFCore.SingleStore.IntegrationTests/Tests/Models/ExpressionTest.cs index 47a0d358a..f01af411b 100644 --- a/test/EFCore.SingleStore.IntegrationTests/Tests/Models/ExpressionTest.cs +++ b/test/EFCore.SingleStore.IntegrationTests/Tests/Models/ExpressionTest.cs @@ -149,7 +149,9 @@ public async Task SingleStoreDateTimeNowTranslator() var utcOffsetStr = (utcOffset.TotalHours >= 0 ? "+" : "") + utcOffset.TotalHours.ToString("00") + ":" + utcOffset.Minutes.ToString("00"); await _db.Database.OpenConnectionAsync(); +#pragma warning disable EF1002 await _db.Database.ExecuteSqlRawAsync($"SET @@session.time_zone = '{utcOffsetStr}'"); +#pragma warning restore EF1002 var result = await _db.DataTypesSimple.Select(m => new { diff --git a/test/EFCore.SingleStore.Tests/AssemblyInfo.cs b/test/EFCore.SingleStore.Tests/AssemblyInfo.cs index 9205dc0df..6714ac906 100644 --- a/test/EFCore.SingleStore.Tests/AssemblyInfo.cs +++ b/test/EFCore.SingleStore.Tests/AssemblyInfo.cs @@ -9,7 +9,7 @@ #if FIXED_TEST_ORDER -[assembly: CollectionBehavior(DisableTestParallelization = true)] +[assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly, DisableTestParallelization = true, MaxParallelThreads = 1)] [assembly: TestCollectionOrderer("EntityFrameworkCore.SingleStore.FunctionalTests.TestUtilities.Xunit.SingleStoreTestCollectionOrderer", "EntityFrameworkCore.SingleStore.FunctionalTests")] [assembly: TestCaseOrderer("EntityFrameworkCore.SingleStore.FunctionalTests.TestUtilities.Xunit.SingleStoreTestCaseOrderer", "EntityFrameworkCore.SingleStore.FunctionalTests")] diff --git a/test/EFCore.SingleStore.Tests/EFCore.SingleStore.Tests.csproj b/test/EFCore.SingleStore.Tests/EFCore.SingleStore.Tests.csproj index 776ca436d..fae1599e4 100644 --- a/test/EFCore.SingleStore.Tests/EFCore.SingleStore.Tests.csproj +++ b/test/EFCore.SingleStore.Tests/EFCore.SingleStore.Tests.csproj @@ -27,7 +27,7 @@ - + diff --git a/test/EFCore.SingleStore.Tests/SingleStoreRelationalConnectionTest.cs b/test/EFCore.SingleStore.Tests/SingleStoreRelationalConnectionTest.cs new file mode 100644 index 000000000..8d14ab95b --- /dev/null +++ b/test/EFCore.SingleStore.Tests/SingleStoreRelationalConnectionTest.cs @@ -0,0 +1,349 @@ +using System; +using System.Data.Common; +using System.Diagnostics; +using System.Reflection; +using System.Transactions; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics.Internal; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Infrastructure.Internal; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Internal; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using SingleStoreConnector; +using EntityFrameworkCore.SingleStore.Diagnostics.Internal; +using EntityFrameworkCore.SingleStore.Infrastructure.Internal; +using EntityFrameworkCore.SingleStore.Internal; +using EntityFrameworkCore.SingleStore.Storage.Internal; +using EntityFrameworkCore.SingleStore.Tests; +using EntityFrameworkCore.SingleStore.TestUtilities.FakeProvider; +using Xunit; + +namespace EntityFrameworkCore.SingleStore; + +public class SingleStoreRelationalConnectionTest +{ + [Fact] + public void Creates_SingleStore_Server_connection_string() + { + using var connection = CreateConnection(); + + Assert.IsType(connection.DbConnection); + } + + /*[Fact] + public void Uses_DbDataSource_from_DbContextOptions() + { + using var dataSource = new SingleStoreDataSourceBuilder("Server=FakeHost;AllowUserVariables=True;UseAffectedRows=False").Build(); + + var serviceCollection = new ServiceCollection(); + + serviceCollection + .AddSingleStoreDataSource("Server=FakeHost") + .AddDbContext(o => o.UseSingleStore(dataSource)); + + using var serviceProvider = serviceCollection.BuildServiceProvider(); + + using var scope1 = serviceProvider.CreateScope(); + var context1 = scope1.ServiceProvider.GetRequiredService(); + var relationalConnection1 = (SingleStoreRelationalConnection)context1.GetService()!; + Assert.Same(dataSource, relationalConnection1.DbDataSource); + + var connection1 = context1.GetService().Database.GetDbConnection(); + Assert.Equal("Server=FakeHost;Allow User Variables=True;Use Affected Rows=False", connection1.ConnectionString); + + using var scope2 = serviceProvider.CreateScope(); + var context2 = scope2.ServiceProvider.GetRequiredService(); + var relationalConnection2 = (SingleStoreRelationalConnection)context2.GetService()!; + Assert.Same(dataSource, relationalConnection2.DbDataSource); + + var connection2 = context2.GetService().Database.GetDbConnection(); + Assert.Equal("Server=FakeHost;Allow User Variables=True;Use Affected Rows=False", connection2.ConnectionString); + } + + [Fact] + public void Uses_DbDataSource_from_DbContextOptions_without_mandatory_settings() + { + using var dataSource = new SingleStoreDataSourceBuilder("Server=FakeHost").Build(); + + var serviceCollection = new ServiceCollection(); + + serviceCollection + .AddSingleStoreDataSource("Server=FakeHost;Allow User Variables=True;Use Affected Rows=False") + .AddDbContext(o => o.UseSingleStore(dataSource)); + + using var serviceProvider = serviceCollection.BuildServiceProvider(); + + using var scope = serviceProvider.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + + Assert.Equal( + "The connection string of a connection used by EntityFrameworkCore.SingleStore must contain \"AllowUserVariables=True;UseAffectedRows=False\".", + Assert.Throws( + () => (SingleStoreRelationalConnection)context.GetService()!) + .Message); + } + + [Fact] + public void Uses_DbDataSource_from_ApplicationServiceProvider() + { + var serviceCollection = new ServiceCollection(); + + serviceCollection + .AddSingleStoreDataSource("Server=FakeHost;Allow User Variables=True;Use Affected Rows=False") + .AddDbContext(o => o.UseSingleStore()); + + using var serviceProvider = serviceCollection.BuildServiceProvider(); + + var dataSource = serviceProvider.GetRequiredService(); + + using var scope1 = serviceProvider.CreateScope(); + var context1 = scope1.ServiceProvider.GetRequiredService(); + var relationalConnection1 = (SingleStoreRelationalConnection)context1.GetService()!; + Assert.Same(dataSource, relationalConnection1.DbDataSource); + + var connection1 = context1.GetService().Database.GetDbConnection(); + Assert.Equal("Server=FakeHost;Allow User Variables=True;Use Affected Rows=False", connection1.ConnectionString); + + using var scope2 = serviceProvider.CreateScope(); + var context2 = scope2.ServiceProvider.GetRequiredService(); + var relationalConnection2 = (SingleStoreRelationalConnection)context2.GetService()!; + Assert.Same(dataSource, relationalConnection2.DbDataSource); + + var connection2 = context2.GetService().Database.GetDbConnection(); + Assert.Equal("Server=FakeHost;Allow User Variables=True;Use Affected Rows=False", connection2.ConnectionString); + } + + [Fact] + public void Uses_DbDataSource_from_ApplicationServiceProvider_without_mandatory_settings() + { + var serviceCollection = new ServiceCollection(); + + serviceCollection + .AddSingleStoreDataSource("Server=FakeHost") + .AddDbContext(o => o.UseSingleStore()); + + using var serviceProvider = serviceCollection.BuildServiceProvider(); + + var dataSource = serviceProvider.GetRequiredService(); + + Assert.Equal("Server=FakeHost", dataSource.ConnectionString); + + using var scope = serviceProvider.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + + Assert.Equal( + "The connection string of a connection used by EntityFrameworkCore.SingleStore must contain \"AllowUserVariables=True;UseAffectedRows=False\".", + Assert.Throws( + () => (SingleStoreRelationalConnection)context.GetService()!) + .Message); + } + + [Fact] + public void Uses_correct_DbDataSource_from_ApplicationServiceProvider_with_cached_DbContextOptions_extension() + { + var serviceCollection1 = new ServiceCollection(); + + serviceCollection1 + .AddSingleStoreDataSource("Server=FakeHost1;Allow User Variables=True;Use Affected Rows=False") + .AddDbContext(o => o.UseSingleStore()); + + using (var serviceProvider1 = serviceCollection1.BuildServiceProvider()) + { + var dataSource1 = serviceProvider1.GetRequiredService(); + Assert.Equal("Server=FakeHost1;Allow User Variables=True;Use Affected Rows=False", dataSource1.ConnectionString); + + var context1 = serviceProvider1.GetRequiredService(); + + var mySqlOptions1 = context1.GetService(); + Assert.Null(mySqlOptions1.DataSource); + + var relationalConnection1 = (SingleStoreRelationalConnection)context1.GetService()!; + Assert.Same(dataSource1, relationalConnection1.DbDataSource); + } + + var serviceCollection2 = new ServiceCollection(); + + serviceCollection2 + .AddSingleStoreDataSource("Server=FakeHost2;Allow User Variables=True;Use Affected Rows=False") + .AddDbContext(o => o.UseSingleStore()); + + using (var serviceProvider2 = serviceCollection2.BuildServiceProvider()) + { + var dataSource2 = serviceProvider2.GetRequiredService(); + Assert.Equal("Server=FakeHost2;Allow User Variables=True;Use Affected Rows=False", dataSource2.ConnectionString); + + var context2 = serviceProvider2.GetRequiredService(); + + var mySqlOptions2 = context2.GetService(); + Assert.Null(mySqlOptions2.DataSource); + + var relationalConnection2 = (SingleStoreRelationalConnection)context2.GetService()!; + Assert.Same(dataSource2, relationalConnection2.DbDataSource); + } + }*/ + + [Fact] + public void Can_create_master_connection_with_connection_string() + { + using var connection = CreateConnection(); + using var master = connection.CreateMasterConnection(); + + var program_version = Assembly.GetExecutingAssembly().GetName().Version.ToString(); + + Assert.Equal( + $@"Server=localhost;User ID=some_user;Password=some_password;Database=;Pooling=False;Allow User Variables=True;Connection Attributes=""_connector_name:SingleStore Entity Framework Core provider,_connector_version:{program_version}"";Use Affected Rows=False", + master.ConnectionString); + } + + // [Fact] + // public void Can_create_master_connection_with_connection_string_and_alternate_admin_db() + // { + // var options = new DbContextOptionsBuilder() + // .UseSingleStore( + // @"Server=localhost;Database=SingleStoreConnectionTest;User ID=some_user;Password=some_password", + // b => b.UseMasterDatabase("template0")) + // .Options; + // + // using var connection = CreateConnection(options); + // using var master = connection.CreateMasterConnection(); + // + // Assert.Equal( + // @"Server=localhost;Database=template0;User ID=some_user;Password=some_password;Pooling=False;Multiplexing=False", + // master.ConnectionString); + // } + + // CHECK: Do we need to fix this test? + // + // [Theory] + // [InlineData("false")] + // [InlineData("False")] + // [InlineData("FALSE")] + // public void CurrentAmbientTransaction_returns_null_with_AutoEnlist_set_to_false(string falseValue) + // { + // var options = new DbContextOptionsBuilder() + // .UseSingleStore( + // @"Server=localhost;Database=SingleStoreConnectionTest;User ID=some_user;Password=some_password;Auto Enlist=" + falseValue, + // AppConfig.ServerVersion) + // .Options; + // + // Transaction.Current = new CommittableTransaction(); + // + // using var connection = CreateConnection(options); + // Assert.Null(connection.CurrentAmbientTransaction); + // + // Transaction.Current = null; + // } + + [Theory] + [InlineData(";Auto Enlist=true")] + [InlineData("")] // Auto Enlist is true by default + public void CurrentAmbientTransaction_returns_transaction_with_AutoEnlist_enabled(string autoEnlist) + { + var options = new DbContextOptionsBuilder() + .UseSingleStore( + @"Server=localhost;Database=SingleStoreConnectionTest;User ID=some_user;Password=some_password" + autoEnlist) + .Options; + + var transaction = new CommittableTransaction(); + Transaction.Current = transaction; + + using var connection = CreateConnection(options); + Assert.Equal(transaction, connection.CurrentAmbientTransaction); + + Transaction.Current = null; + } + + // INFO: We currently don't implement ISingleStoreRelationalConnection.CloneWith. + // + // [ConditionalFact] + // public void CloneWith_with_connection_and_connection_string() + // { + // var services = SingleStoreTestHelpers.Instance.CreateContextServices( + // new DbContextOptionsBuilder() + // .UseSingleStore("Server=localhost;Database=DummyDatabase", AppConfig.ServerVersion) + // .Options); + // + // var relationalConnection = (ISingleStoreRelationalConnection)services.GetRequiredService(); + // + // var clone = relationalConnection.CloneWith("Server=localhost;Database=DummyDatabase;Application Name=foo"); + // + // Assert.Equal("Server=localhost;Database=DummyDatabase;Application Name=foo", clone.ConnectionString); + // } + + private static SingleStoreRelationalConnection CreateConnection(DbContextOptions options = null, DbDataSource dataSource = null) + { + options ??= new DbContextOptionsBuilder() + .UseSingleStore(@"Server=localhost;User ID=some_user;Password=some_password;Database=SingleStoreConnectionTest") + .Options; + + foreach (var extension in options.Extensions) + { + extension.Validate(options); + } + + var singletonOptions = new SingleStoreOptions(); + singletonOptions.Initialize(options); + + return new SingleStoreRelationalConnection( + new RelationalConnectionDependencies( + options, + new DiagnosticsLogger( + new LoggerFactory(), + new LoggingOptions(), + new DiagnosticListener("FakeDiagnosticListener"), + new SingleStoreLoggingDefinitions(), + new NullDbContextLogger()), + new RelationalConnectionDiagnosticsLogger( + new LoggerFactory(), + new LoggingOptions(), + new DiagnosticListener("FakeDiagnosticListener"), + new SingleStoreLoggingDefinitions(), + new NullDbContextLogger(), + options), + new NamedConnectionStringResolver(options), + new RelationalTransactionFactory( + new RelationalTransactionFactoryDependencies( + new RelationalSqlGenerationHelper( + new RelationalSqlGenerationHelperDependencies()))), + new CurrentDbContext(new FakeDbContext()), + new RelationalCommandBuilderFactory( + new RelationalCommandBuilderDependencies( + new SingleStoreTypeMappingSource( + TestServiceFactory.Instance.Create(), + TestServiceFactory.Instance.Create(), + singletonOptions), + new ExceptionDetector()))), + new SingleStoreConnectionStringOptionsValidator(), + singletonOptions); + } + + private const string ConnectionString = "Fake Connection String"; + + private static IDbContextOptions CreateOptions( + RelationalOptionsExtension optionsExtension = null) + { + var optionsBuilder = new DbContextOptionsBuilder(); + + ((IDbContextOptionsBuilderInfrastructure)optionsBuilder) + .AddOrUpdateExtension( + optionsExtension + ?? new FakeRelationalOptionsExtension().WithConnectionString(ConnectionString)); + + return optionsBuilder.Options; + } + + private class FakeDbContext : DbContext + { + public FakeDbContext() + { + } + + public FakeDbContext(DbContextOptions options) + : base(options) + { + } + } +} diff --git a/test/EFCore.SingleStore.Tests/TestUtilities/FakeProvider/FakeRelationalOptionsExtension.cs b/test/EFCore.SingleStore.Tests/TestUtilities/FakeProvider/FakeRelationalOptionsExtension.cs new file mode 100644 index 000000000..851093743 --- /dev/null +++ b/test/EFCore.SingleStore.Tests/TestUtilities/FakeProvider/FakeRelationalOptionsExtension.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.Extensions.DependencyInjection; + +namespace EntityFrameworkCore.SingleStore.TestUtilities.FakeProvider; + +public class FakeRelationalOptionsExtension : RelationalOptionsExtension +{ + private DbContextOptionsExtensionInfo _info; + + public FakeRelationalOptionsExtension() + { + } + + protected FakeRelationalOptionsExtension(FakeRelationalOptionsExtension copyFrom) + : base(copyFrom) + { + } + + public override DbContextOptionsExtensionInfo Info + => _info ??= new ExtensionInfo(this); + + protected override RelationalOptionsExtension Clone() + => new FakeRelationalOptionsExtension(this); + + public override void ApplyServices(IServiceCollection services) + => AddEntityFrameworkRelationalDatabase(services); + + public static IServiceCollection AddEntityFrameworkRelationalDatabase(IServiceCollection serviceCollection) + { + var builder = new EntityFrameworkRelationalServicesBuilder(serviceCollection); + + // Specific test services are available upstream if we need them + builder.TryAddCoreServices(); + + return serviceCollection; + } + + private sealed class ExtensionInfo : RelationalExtensionInfo + { + public ExtensionInfo(IDbContextOptionsExtension extension) + : base(extension) + { + } + + public override void PopulateDebugInfo(IDictionary debugInfo) + { + } + } +} diff --git a/test/EFCore.SingleStore.Tests/TestUtilities/TestRelationalTypeMappingSource.cs b/test/EFCore.SingleStore.Tests/TestUtilities/TestRelationalTypeMappingSource.cs index b89027b70..7c4279d4b 100644 --- a/test/EFCore.SingleStore.Tests/TestUtilities/TestRelationalTypeMappingSource.cs +++ b/test/EFCore.SingleStore.Tests/TestUtilities/TestRelationalTypeMappingSource.cs @@ -7,6 +7,7 @@ using System.Linq; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Json; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Microsoft.EntityFrameworkCore.TestUtilities @@ -64,11 +65,16 @@ private IntArrayTypeMapping(RelationalTypeMappingParameters parameters) { } - public override RelationalTypeMapping Clone(string storeType, int? size) + public override RelationalTypeMapping WithStoreTypeAndSize(string storeType, int? size) => new IntArrayTypeMapping(Parameters.WithStoreTypeAndSize(storeType, size)); - public override CoreTypeMapping Clone(ValueConverter converter) - => new IntArrayTypeMapping(Parameters.WithComposedConverter(converter)); + public override CoreTypeMapping WithComposedConverter( + ValueConverter converter, + ValueComparer comparer = null, + ValueComparer keyComparer = null, + CoreTypeMapping elementMapping = null, + JsonValueReaderWriter jsonValueReaderWriter = null) + => new IntArrayTypeMapping(Parameters.WithComposedConverter(converter, comparer, keyComparer, elementMapping, jsonValueReaderWriter)); protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) => new IntArrayTypeMapping(parameters); @@ -198,7 +204,7 @@ protected override RelationalTypeMapping FindMapping(in RelationalTypeMappingInf { return storeTypeName != null && !mapping.StoreType.Equals(storeTypeName, StringComparison.Ordinal) - ? mapping.Clone(storeTypeName, mapping.Size) + ? mapping.WithStoreTypeAndSize(storeTypeName, mapping.Size) : mapping; } } diff --git a/test/Shared/AppConfig.cs b/test/Shared/AppConfig.cs index 5ed55c56d..119f032be 100644 --- a/test/Shared/AppConfig.cs +++ b/test/Shared/AppConfig.cs @@ -4,7 +4,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using SingleStoreConnector; -using EntityFrameworkCore.SingleStore.Storage; namespace EntityFrameworkCore.SingleStore.Tests { diff --git a/tools/QueryBaselineUpdater/QueryBaselineUpdater.csproj b/tools/QueryBaselineUpdater/QueryBaselineUpdater.csproj index 82cad6d5e..f1fa7fe2c 100644 --- a/tools/QueryBaselineUpdater/QueryBaselineUpdater.csproj +++ b/tools/QueryBaselineUpdater/QueryBaselineUpdater.csproj @@ -2,7 +2,7 @@ Exe - net7.0 + net8.0