efcore icon indicating copy to clipboard operation
efcore copied to clipboard

Unhandled exception when calling ExecuteUpdate on TPС hierarchy

Open TonEnfer opened this issue 8 months ago • 1 comments

Bug description

An unhandled exception occurs when calling ExecuteUpdateAsync on a TPC hierarchy

Your code

using Microsoft.EntityFrameworkCore;

namespace NullabilityProcessorBug;

class Program
{
    static async Task Main(string[] args)
    {
        await using (var context = new BugDbContext())
        {
            await context.Database.MigrateAsync();
            context.Set<TreeItem>().AddRange(_getSeedData());
            await context.SaveChangesAsync();
        }

        await using (var context = new BugDbContext())
        {
            IQueryable<TreeItem> itemsQuery = context.Set<TreeItem>();

            var list = await context
                .Set<TreeItemA>()
                .Select(x => new
                    {
                        Item = x,
                        Parent = itemsQuery.FirstOrDefault(parent => parent.Id == x.ParentId)
                    }
                )
                .Select(x => new
                    {
                        Item = x.Item,
                        Path = x.Item.ParentId.HasValue && x.Parent != null
                            ? (LTree)(x.Parent.TreePath + "." + x.Item.Id.ToString().Replace("-", string.Empty).ToLower())
                            : (LTree)x.Item.Id.ToString().Replace("-", string.Empty).ToLower()
                    }
                )
                .ExecuteUpdateAsync(x => x
                    .SetProperty(
                        y => y.Item.TreePath,
                        y => y.Path
                    )
                );
        }

        Console.WriteLine("Hello, World!");
    }

    private static IEnumerable<TreeItem> _getSeedData()
    {
        return
        [
            new TreeItemA
            {
                Id = Guid.CreateVersion7(),
                Children =
                [
                    new TreeItemB
                    {
                        Id = Guid.CreateVersion7(),
                    },
                    new TreeItemB
                    {
                        Id = Guid.CreateVersion7(),
                    },
                    new TreeItemA
                    {
                        Id = Guid.CreateVersion7(),
                        Children =
                        [
                            new TreeItemB
                            {
                                Id = Guid.CreateVersion7(),
                            },
                            new TreeItemB
                            {
                                Id = Guid.CreateVersion7(),
                            }
                        ]
                    }
                ]
            }
        ];
    }
}

class BugDbContext: DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseNpgsql("Host=localhost;Database=NullabilityProcessorBug;Username=postgres;Password=postgres;Port=5432");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder
            .Entity<TreeItem>()
            .UseTpcMappingStrategy()
            .HasMany(x => x.Children)
            .WithOne()
            .HasForeignKey(x => x.ParentId)
            .IsRequired(false);

        modelBuilder
            .Entity<TreeItemA>();

        modelBuilder.Entity<TreeItemA>();
        modelBuilder.Entity<TreeItemB>();
    }
}

abstract class TreeItem
{
    public Guid Id { get; set; }

    public Guid? ParentId { get; set; }

    public LTree TreePath { get; set; } = new LTree("unsetted");

    public IList<TreeItem> Children { get; set; } = new List<TreeItem>();
}

class TreeItemA: TreeItem;

class TreeItemB: TreeItem;

Stack traces

System.InvalidOperationException: Unhandled expression '[Microsoft.EntityFrameworkCore.Query.Internal.TpcTablesExpression]' of type 'Microsoft.EntityFrameworkCore.Query.Internal.TpcTablesExpression' encountered in 'SqlNullabilityProcessor'.
   at Microsoft.EntityFrameworkCore.Query.SqlNullabilityProcessor.Visit(TableExpressionBase tableExpressionBase)
   at Microsoft.EntityFrameworkCore.Query.SqlNullabilityProcessor.Visit(SelectExpression selectExpression, Boolean visitProjection)
   at Microsoft.EntityFrameworkCore.Query.SqlNullabilityProcessor.Visit(SelectExpression selectExpression)
   at Microsoft.EntityFrameworkCore.Query.SqlNullabilityProcessor.VisitExists(ExistsExpression existsExpression, Boolean allowOptimizedExpansion, Boolean& nullable)
   at Microsoft.EntityFrameworkCore.Query.SqlNullabilityProcessor.Visit(SqlExpression sqlExpression, Boolean allowOptimizedExpansion, Boolean preserveColumnNullabilityInformation, Boolean& nullable)
   at Microsoft.EntityFrameworkCore.Query.SqlNullabilityProcessor.VisitSqlBinary(SqlBinaryExpression sqlBinaryExpression, Boolean allowOptimizedExpansion, Boolean& nullable)
   at Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal.NpgsqlSqlNullabilityProcessor.VisitSqlBinary(SqlBinaryExpression sqlBinaryExpression, Boolean allowOptimizedExpansion, Boolean& nullable)
   at Microsoft.EntityFrameworkCore.Query.SqlNullabilityProcessor.Visit(SqlExpression sqlExpression, Boolean allowOptimizedExpansion, Boolean preserveColumnNullabilityInformation, Boolean& nullable)
   at Microsoft.EntityFrameworkCore.Query.SqlNullabilityProcessor.VisitCase(CaseExpression caseExpression, Boolean allowOptimizedExpansion, Boolean& nullable)
   at Microsoft.EntityFrameworkCore.Query.SqlNullabilityProcessor.Visit(SqlExpression sqlExpression, Boolean allowOptimizedExpansion, Boolean preserveColumnNullabilityInformation, Boolean& nullable)
   at Microsoft.EntityFrameworkCore.Query.SqlNullabilityProcessor.Visit(SqlExpression sqlExpression, Boolean allowOptimizedExpansion, Boolean& nullable)
   at Microsoft.EntityFrameworkCore.Query.SqlNullabilityProcessor.Visit(SqlExpression sqlExpression, Boolean& nullable)
   at Microsoft.EntityFrameworkCore.Query.SqlNullabilityProcessor.VisitUpdate(UpdateExpression updateExpression)
   at Microsoft.EntityFrameworkCore.Query.SqlNullabilityProcessor.Process(Expression queryExpression, IReadOnlyDictionary`2 parameterValues, Boolean& canCache)
   at Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal.NpgsqlParameterBasedSqlProcessor.ProcessSqlNullability(Expression selectExpression, IReadOnlyDictionary`2 parametersValues, Boolean& canCache)
   at Microsoft.EntityFrameworkCore.Query.RelationalParameterBasedSqlProcessor.Optimize(Expression queryExpression, IReadOnlyDictionary`2 parametersValues, Boolean& canCache)
   at Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal.NpgsqlParameterBasedSqlProcessor.Optimize(Expression queryExpression, IReadOnlyDictionary`2 parametersValues, Boolean& canCache)
   at Microsoft.EntityFrameworkCore.Query.Internal.RelationalCommandCache.GetRelationalCommandTemplate(IReadOnlyDictionary`2 parameters)
   at lambda_method60(Closure, IReadOnlyDictionary`2)
   at Microsoft.EntityFrameworkCore.Internal.RelationalCommandResolverExtensions.RentAndPopulateRelationalCommand(RelationalCommandResolver relationalCommandResolver, RelationalQueryContext queryContext)
   at Microsoft.EntityFrameworkCore.Query.RelationalShapedQueryCompilingExpressionVisitor.<>c.<NonQueryResultAsync>b__32_0(DbContext _, ValueTuple`3 state, CancellationToken cancellationToken)
   at Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.NpgsqlExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
   at NullabilityProcessorBug.Program.Main(String[] args) in D:\_repos\NullabilityProcessorBug\NullabilityProcessorBug\Program.cs:line 20
   at NullabilityProcessorBug.Program.Main(String[] args) in D:\_repos\NullabilityProcessorBug\NullabilityProcessorBug\Program.cs:line 42
   at NullabilityProcessorBug.Program.<Main>(String[] args)

Verbose output


EF Core version

9.0.5

Database provider

Npgsql.EntityFrameworkCore.PostgreSQL

Target framework

.NET 9.0

Operating system

Windows 11

IDE

No response

TonEnfer avatar Jun 09 '25 18:06 TonEnfer

ExecuteUpdateAsync does not currently support modifying more than one table (e.g. TPT, TPC). In most cases such scenarios are detected and a specific, informative exception is thrown, but you're trying to do something a bit more complicated than the usual (mix TPC with LTree etc.).

Putting on the backlog to investigate and correct the exception.

(previously opened as https://github.com/npgsql/efcore.pg/issues/3544)

roji avatar Jun 09 '25 21:06 roji