dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.63k stars 3.15k forks source link

Function schema name is lost when AsSplitQuery() is applied #34341

Closed llRandom closed 2 weeks ago

llRandom commented 1 month ago

File a bug

Function schema name is lost when AsSplitQuery() is applied. If AsSplitQuery() call is removed query runs fine and returns results. If I keep AsSplitQuery() call and create database function in public schema it also works fine. But if function is defined in another schema (common in this example) it doesn't work because produced query doesn't contain schema name

Reproducible demo

Models

    public class MatchingScore
    {
        public int Id { get; set; }
        public double MatchScore { get; set; }
    }

    public class Company
    {
        public int Id { get; set; }
        public required string Name { get; set; }
        public List<CompanyCompanyType>? CompanyTypes { get; set; }
    }

    public class CompanyType
    {
        public int Id { get; set; }
        public required string Name { get; set; }
    }

    public class CompanyCompanyType
    {
        public int CompanyTypeId { get; set; }
        public CompanyType? CompanyType { get; set; }
        public int CompanyId { get; set; }
        public Company? Company { get; set; }
    }

Context

    public class EFContext : DbContext
    {
        public IQueryable<MatchingScore> MatchingScore(int matchCompanyId, int? id1, int? id2, string json)
            => throw new NotSupportedException();

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSnakeCaseNamingConvention();
            optionsBuilder.UseNpgsql("Server=localhost;Database=SplitQueryBug;Port=5432;User Id=postgres;Password=local;Ssl Mode=Prefer;Include Error Detail=true;");
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            modelBuilder.Entity<Company>().ToTable("company", "company");
            modelBuilder.Entity<CompanyType>().ToTable("company_type", "company");
            modelBuilder.Entity<CompanyCompanyType>().ToTable("company_company_type", "company").HasKey(m => new { m.CompanyId, m.CompanyTypeId });
            modelBuilder.HasDbFunction(typeof(EFContext).GetMethod(nameof(MatchingScore), new[] { typeof(int), typeof(int?), typeof(int?), typeof(string) })!)
                .HasSchema("common")
                .HasName("matching_score");
        }
    }

Query

using var context = new EFContext();
var query = from company in context.Set<Company>()
            from match in context.MatchingScore(company.Id, 1, 1, "[]")
            select new
            {
                company.Id,
                company.CompanyTypes,
                match.MatchScore
            };
var rows = query.AsSplitQuery().ToList();

Include stack traces

Npgsql.PostgresException
  HResult=0x80004005
  Message=42883: function matching_score(integer, integer, integer, unknown) does not exist

POSITION: 95
  Source=Npgsql
  StackTrace:
   at Npgsql.Internal.NpgsqlConnector.<ReadMessageLong>d__233.MoveNext()
   at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
   at Npgsql.NpgsqlDataReader.<NextResult>d__52.MoveNext()
   at Npgsql.NpgsqlDataReader.<NextResult>d__52.MoveNext()
   at Npgsql.NpgsqlDataReader.NextResult()
   at Npgsql.NpgsqlCommand.<ExecuteReader>d__119.MoveNext()
   at Npgsql.NpgsqlCommand.<ExecuteReader>d__119.MoveNext()
   at System.Threading.Tasks.ValueTask`1.get_Result()
   at Npgsql.NpgsqlCommand.ExecuteReader(CommandBehavior behavior)
   at Npgsql.NpgsqlCommand.ExecuteDbDataReader(CommandBehavior behavior)
   at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReader(RelationalCommandParameterObject parameterObject)
   at Microsoft.EntityFrameworkCore.Query.RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.<PopulateSplitCollection>g__InitializeReader|30_1[TCollection,TElement,TRelatedEntity](RelationalQueryContext queryContext, RelationalCommandCache relationalCommandCache, IReadOnlyList`1 readerColumns, Boolean detailedErrorsEnabled)
   at Microsoft.EntityFrameworkCore.Query.RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.<>c__30`3.<PopulateSplitCollection>b__30_0(ValueTuple`4 tup)
   at Microsoft.EntityFrameworkCore.ExecutionStrategyExtensions.<>c__DisplayClass12_0`2.<Execute>b__0(DbContext _, TState s)
   at Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.NpgsqlExecutionStrategy.Execute[TState,TResult](TState state, Func`3 operation, Func`3 verifySucceeded)
   at Microsoft.EntityFrameworkCore.ExecutionStrategyExtensions.Execute[TState,TResult](IExecutionStrategy strategy, TState state, Func`2 operation, Func`2 verifySucceeded)
   at Microsoft.EntityFrameworkCore.Query.RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.PopulateSplitCollection[TCollection,TElement,TRelatedEntity](Int32 collectionId, RelationalQueryContext queryContext, IExecutionStrategy executionStrategy, RelationalCommandCache relationalCommandCache, IReadOnlyList`1 readerColumns, Boolean detailedErrorsEnabled, SplitQueryResultCoordinator resultCoordinator, Func`3 childIdentifier, IReadOnlyList`1 identifierValueComparers, Func`5 innerShaper, Action`3 relatedDataLoaders)
   at Microsoft.EntityFrameworkCore.Query.Internal.SplitQueryingEnumerable`1.Enumerator.MoveNext()
   at System.Collections.Generic.List`1..ctor(IEnumerable`1 collection)
   at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
   at Program.<Main>$(String[] args) in C:\Users\eugene\Projects\SplitQueryBug\Program.cs:line 15

Include provider and version information

EF Core version: 8.0.7 Database provider: Npgsql.EntityFrameworkCore.PostgreSQL 8.0.4 Target framework: .Net 8.0 Operating system: Windows 11 (Version 10.0.22631.3880) IDE: Visual Studio 2022 17.9.3

roji commented 1 month ago

Confirmed. With the full repro below, EF generates the following SQL:

-- First query: function is schema-qualified
SELECT c."Id", m."Id", m."MatchScore"
FROM company.company AS c
JOIN LATERAL common.matching_score(c."Id", 1, 1, '[]') AS m ON TRUE
ORDER BY c."Id", m."Id"

-- Second query: function isn't schema-qualified
SELECT c0."CompanyId", c0."CompanyTypeId", c."Id", m."Id"
FROM company.company AS c
JOIN LATERAL matching_score(c."Id", 1, 1, '[]') AS m ON TRUE
INNER JOIN company.company_company_type AS c0 ON c."Id" = c0."CompanyId"
ORDER BY c."Id", m."Id"
Full repro ```c# await using var context = new EFContext(); await context.Database.EnsureDeletedAsync(); await context.Database.EnsureCreatedAsync(); await context.Database.ExecuteSqlAsync( $""" CREATE SCHEMA common; CREATE FUNCTION common.matching_score(match_company_id integer, id1 integer, id2 integer, json jsonb) RETURNS TABLE("Id" integer, "MatchScore" double precision) AS $$ SELECT "Id", random() AS "MatchScore" FROM company.company WHERE "Id" = match_company_id; $$ LANGUAGE SQL; """); context.Set().Add(new Company { Name = "foo" }); await context.SaveChangesAsync(); var query = from company in context.Set() from match in context.MatchingScore(company.Id, 1, 1, "[]") select new { company.Id, company.CompanyTypes, match.MatchScore }; var rows = query.AsSplitQuery().ToList(); public class EFContext : DbContext { public IQueryable MatchingScore(int matchCompanyId, int? id1, int? id2, string json) => throw new NotSupportedException(); protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder .UseNpgsql("Host=localhost;Username=test;Password=test") .LogTo(Console.WriteLine, LogLevel.Information) .EnableSensitiveDataLogging(); protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity().ToTable("company", "company"); modelBuilder.Entity().ToTable("company_type", "company"); modelBuilder.Entity().ToTable("company_company_type", "company").HasKey(m => new { m.CompanyId, m.CompanyTypeId }); modelBuilder.HasDbFunction(typeof(EFContext).GetMethod(nameof(MatchingScore), new[] { typeof(int), typeof(int?), typeof(int?), typeof(string) })!) .HasSchema("common") .HasName("matching_score"); } } public class MatchingScore { public int Id { get; set; } public double MatchScore { get; set; } } public class Company { public int Id { get; set; } public required string Name { get; set; } public List? CompanyTypes { get; set; } } public class CompanyType { public int Id { get; set; } public required string Name { get; set; } } public class CompanyCompanyType { public int CompanyTypeId { get; set; } public CompanyType? CompanyType { get; set; } public int CompanyId { get; set; } public Company? Company { get; set; } } ```
roji commented 2 weeks ago

Confirmed that this was fixed for 9.0.0 as part of #32815 (https://github.com/dotnet/efcore/pull/32815/files#diff-e56f6f490873a5c9d0bae06280f5c3d1d48ffa9b339b62521afaa61ecae6f5e9R137).