Skip to content

GroupBy generates invalid SQL when using custom database functionΒ #29638

@andreikarkkanen

Description

@andreikarkkanen

When executing the following query an exception is thrown

var orderIds = new [] { Guid.NewGuid(), Guid.NewGuid() };

var orders = dbContext.Orders
    .Where(c => dbContext.AsQueryable(orderIds).Contains(c.OrderId))
    .Select(c => new
    {
        c.Project.Code,
        c.Project.Revenue
    })
    .GroupBy(c => new
    {
        c.Code
    })
    .Select(c => new
    {
        c.Key.Code,
        Sum = c.Sum(e => e.Revenue)
    }).ToList();

Microsoft.Data.SqlClient.SqlException (0x80131904): The multi-part identifier "s.Value" could not be bound.

exec sp_executesql N'SELECT [p].[Code], (
    SELECT COALESCE(SUM([p1].[Revenue]), 0.0)
    FROM [Orders] AS [o0]
    INNER JOIN [Projects] AS [p0] ON [o0].[ProjectId] = [p0].[ProjectId]
    INNER JOIN [Projects] AS [p1] ON [o0].[ProjectId] = [p1].[ProjectId]
    WHERE EXISTS (
        SELECT 1
        FROM STRING_SPLIT(@__source_1, @__separator_2) AS [s0]
        WHERE CONVERT(UNIQUEIDENTIFIER, [s0].[Value]) = [o0].[OrderId]) AND [p].[Code] = [p0].[Code]) AS [Sum]
FROM [Orders] AS [o]
INNER JOIN [Projects] AS [p] ON [o].[ProjectId] = [p].[ProjectId]
WHERE EXISTS (
    SELECT 1
    FROM STRING_SPLIT(@__source_1, @__separator_2) AS [s0]
    WHERE CONVERT(UNIQUEIDENTIFIER, [s].[Value]) = [o].[OrderId])
GROUP BY [p].[Code]',N'@__source_1 nvarchar(4000),@__separator_2 nvarchar(4000)',@__source_1=N'7bd14254-eac8-413d-9328-2fda4725b690,e624c495-55dd-436a-8c62-6a7e49c8435d',@__separator_2=N','

The incorrect part is

WHERE CONVERT(UNIQUEIDENTIFIER, [s].[Value]) = [o].[OrderId]

The .AsQueriable() solves a cache plan pollution problem. The full solution is in the attachment.

ConsoleApp1.zip

Or

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;

var optionsBuilder = new DbContextOptionsBuilder<TestContext>();
optionsBuilder.UseSqlServer("Server=TestDb;Database=CustomerDb;Trusted_Connection=True;Encrypt=False;");

using var dbContext = new TestContext(optionsBuilder.Options);
dbContext.Database.EnsureDeleted();
dbContext.Database.EnsureCreated();

var orderIds = new [] { Guid.NewGuid(), Guid.NewGuid() };

var orders = dbContext.Orders
    .Where(c => dbContext.AsQueryable(orderIds).Contains(c.OrderId))
    .Select(c => new
    {
        c.Project.Code,
        c.Project.Revenue
    })
    .GroupBy(c => new
    {
        c.Code
    })
    .Select(c => new
    {
        c.Key.Code,
        Sum = c.Sum(e => e.Revenue)
    }).ToList();

public class Project
{
    public Guid ProjectId { get; set; }
    public decimal Revenue { get; set; }
    public string Code { get; set; }
}

public class Order
{
    public Guid OrderId { get; set; }
    public Guid ProjectId { get; set; }

    public Project Project { get; set; }
}

public class TestContext : DbContext
{
    public TestContext(DbContextOptions options)
        : base(options)
    {
    }

    public DbSet<Project> Projects { get; set; }
    public DbSet<Order> Orders { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.ConfigureDbFunctions();
    }

    [Keyless]
    private class StringSplitResult
    {
        public string Value { get; set; }
    }
        
    [DbFunction(IsBuiltIn = true, Name = "STRING_SPLIT")]
    private IQueryable<StringSplitResult> Split(string source, string separator)
        => FromExpression(() => Split(source, separator));

    public IQueryable<Guid> AsQueryable(IEnumerable<Guid> source)
        => Split(string.Join(",", source.Select(x => Convert.ToString(x))), ",")
            .Select(s => DbFunctionExtensions.ToGuid(s.Value)!.Value);
}

public static class DbFunctionExtensions
{
    public static Guid? ToGuid(string value) => throw new NotImplementedException();

    public static void ConfigureDbFunctions(this ModelBuilder modelBuilder)
	{
		var type = typeof(DbFunctionExtensions);

        modelBuilder.HasDbFunction(type.GetMethod(nameof(ToGuid))!)
			.HasTranslation(t => new SqlFunctionExpression(
				functionName: "CONVERT",
				arguments: new[] { new SqlFragmentExpression("UNIQUEIDENTIFIER"), t[0] },
				nullable: true,
				argumentsPropagateNullability: new[] { false, true },
				typeof(Guid),
				typeMapping: null))
			.IsBuiltIn();
    }
}

Environment details:
EF Core: 7.0.0
Database provider: Microsoft.EntityFrameworkCore.SqlServer
Target framework: .NET 7.0
Operating system: Windows 10
IDE: Visual Studio 2022 17.4.1

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions