Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EF Core 7.0 generates query with non-existent columns for composite foreign key #29697

Closed
scobei opened this issue Nov 29, 2022 · 5 comments
Closed

Comments

@scobei
Copy link

scobei commented Nov 29, 2022

Issue description

If a table references a composite unique constraint, EF Core 7.0 generates a query that includes columns that do not exist. Previous versions did not have this problem. Below is a minimal example to reproduce the problem.

Database tables

-- ----------------------------
-- base_table
-- ----------------------------
DROP TABLE IF EXISTS "public"."base_table";
CREATE TABLE "public"."base_table" (
  "base_id" int8 NOT NULL,
  "base_type_id" int4 NOT NULL
);

ALTER TABLE "public"."base_table" ADD CONSTRAINT "ak_base" UNIQUE ("base_id", "base_type_id");

ALTER TABLE "public"."base_table" ADD CONSTRAINT "base_table_pkey" PRIMARY KEY ("base_id");

-- ----------------------------
-- sub_table
-- ----------------------------
DROP TABLE IF EXISTS "public"."sub_table";
CREATE TABLE "public"."sub_table" (
  "sub_id" int8 NOT NULL,
  "base_type_id" int4 NOT NULL
);

ALTER TABLE "public"."sub_table" ADD CONSTRAINT "sub_table_pkey" PRIMARY KEY ("sub_id");

ALTER TABLE "public"."sub_table" ADD CONSTRAINT "fk_sub_base_one_to_one" FOREIGN KEY ("sub_id", "base_type_id") REFERENCES "public"."base_table" ("base_id", "base_type_id") ON DELETE CASCADE ON UPDATE CASCADE;

Program.cs

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using test.Data;

var optionsBuilder = new DbContextOptionsBuilder<TestDbContext>();
optionsBuilder.UseNpgsql("...")
    .LogTo(Console.WriteLine, LogLevel.Information)
    .EnableSensitiveDataLogging();

using (var db = new TestDbContext(optionsBuilder.Options))
{
    var entity = db.SubTables.SingleOrDefault(x => x.SubId == 1);
    Console.WriteLine($"SubId: {entity!.SubId}, BaseTypeId: {entity!.BaseTypeId}");
}

Output for EF Core: 6.0.11, Npgsql: 6.0.7

info: 11/29/2022 08:51:06.363 CoreEventId.ContextInitialized[10403] (Microsoft.EntityFrameworkCore.Infrastructure)
      Entity Framework Core 6.0.11 initialized 'TestDbContext' using provider 'Npgsql.EntityFrameworkCore.PostgreSQL:6.0.7+5b98e4b74de93b590b44aef8dd7967c03acc433c' with options: SensitiveDataLoggingEnabled
info: 11/29/2022 08:51:06.685 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (26ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT s.sub_id, s.base_type_id
      FROM sub_table AS s
      WHERE s.sub_id = 1
      LIMIT 2
SubId: 1, BaseTypeId: 1

Output for EF Core: 7.0.0, Npgsql: 7.0.0

fail: 11/29/2022 08:53:15.024 RelationalEventId.CommandError[20102] (Microsoft.EntityFrameworkCore.Database.Command)
      Failed executing DbCommand (18ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT s.sub_id, s."BaseTableBaseId", s."BaseTableBaseTypeId", s.base_type_id
      FROM sub_table AS s
      WHERE s.sub_id = 1
      LIMIT 2
fail: 11/29/2022 08:53:15.046 CoreEventId.QueryIterationFailed[10100] (Microsoft.EntityFrameworkCore.Query)
      An exception occurred while iterating over the results of a query for context type 'test.Data.TestDbContext'.
      Npgsql.PostgresException (0x80004005): 42703: column s.BaseTableBaseId does not exist

      POSITION: 18
         at Npgsql.Internal.NpgsqlConnector.<ReadMessage>g__ReadMessageLong|222_0(NpgsqlConnector connector, Boolean async, DataRowLoadingMode dataRowLoadingMode, Boolean readingNotifications, Boolean isReadingPrependedMessage)
         at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
         at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
         at Npgsql.NpgsqlDataReader.NextResult()
         at Npgsql.NpgsqlCommand.ExecuteReader(CommandBehavior behavior, Boolean async, CancellationToken cancellationToken)
         at Npgsql.NpgsqlCommand.ExecuteReader(CommandBehavior behavior, Boolean async, CancellationToken cancellationToken)
         at Npgsql.NpgsqlCommand.ExecuteReader(CommandBehavior behavior)
         at Npgsql.NpgsqlCommand.ExecuteDbDataReader(CommandBehavior behavior)
         at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReader(RelationalCommandParameterObject parameterObject)
         at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.Enumerator.InitializeReader(Enumerator enumerator)
         at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.Enumerator.<>c.<MoveNext>b__21_0(DbContext _, Enumerator enumerator)
         at Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.NpgsqlExecutionStrategy.Execute[TState,TResult](TState state, Func`3 operation, Func`3 verifySucceeded)
         at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.Enumerator.MoveNext()
        Exception data:
          Severity: ERROR
          SqlState: 42703
          MessageText: column s.BaseTableBaseId does not exist
          Position: 18
          File: d:\pginstaller_13.auto\postgres.windows-x64\src\backend\parser\parse_relation.c
          Line: 3514
          Routine: errorMissingColumn

Provider and version information

EF Core version: 7.0.0
Database provider: Npgsql.EntityFrameworkCore.PostgreSQL
Target framework: .NET 7.0
Operating system: Windows 11 Pro for Workstations, version 22H2
IDE: Visual Studio 2022 17.4.1

@ajcvickers
Copy link
Contributor

This issue is lacking enough information for us to be able to fully understand what is happening. In particular, it doesn't contain code for the entity types or model configuration. Please attach a small, runnable project or post a small, runnable code listing that reproduces what you are seeing so that we can investigate.

@scobei
Copy link
Author

scobei commented Nov 30, 2022

Sure. Here's the project: https://github.com/scobei/efcore7_composite_fk_issue

Steps to reproduce

Step 1 - Add tables to database

Add the two tables base_table and sub_table described above. I am using PostgreSQL 13, by the way.

Step 2 - Scaffold

The command I used is:

dotnet ef dbcontext scaffold "connection_string" Npgsql.EntityFrameworkCore.PostgreSQL --no-onconfiguring --force --project efcore7_composite_fk_issue --context TestDbContext --context-dir autogen/Data --context-namespace test.Data --output-dir autogen/Data/Models --namespace test.Data.Models --data-annotations

Step 3 - Build and run

@ajcvickers
Copy link
Contributor

ajcvickers commented Dec 5, 2022

Note for triage: repros with SQL Server. There is code below to generate the SQL Server database as well as the results of reverse engineering the database. Looks like a reverse engineering issue--the HasForeignKey call for the composite key is missing. Could be related to #29418.

@scobei As a workaround, you can update the scaffolded code to add in the missing HasForeignKey call:

entity.HasOne(d => d.BaseTable).WithMany(p => p.SubTables)
    .HasForeignKey(p => new { p.SubId, p.BaseTypeId })
    .HasPrincipalKey(p => new { p.BaseId, p.BaseTypeId })
    .HasConstraintName("fk_sub_base_one_to_one");

The generated model is:

Model: 
  EntityType: BaseTable
    Properties: 
      BaseId (long) Required PK Index AfterSave:Throw
      BaseTypeId (int) Required AlternateKey Index AfterSave:Throw
    Navigations: 
      SubTables (ICollection<SubTable>) Collection ToDependent SubTable Inverse: BaseTable
    Keys: 
      BaseId PK
      BaseId, BaseTypeId
    Indexes: 
      BaseId, BaseTypeId AK_base_table_base_id_base_type_id Unique
  EntityType: SubTable
    Properties: 
      SubId (long) Required PK AfterSave:Throw
      BaseTableBaseId (no field, long?) Shadow FK Index
      BaseTableBaseTypeId (no field, int?) Shadow FK Index
      BaseTypeId (int) Required
    Navigations: 
      BaseTable (BaseTable) ToPrincipal BaseTable Inverse: SubTables
    Keys: 
      SubId PK
    Foreign keys: 
      SubTable {'BaseTableBaseId', 'BaseTableBaseTypeId'} -> BaseTable {'BaseId', 'BaseTypeId'} ToDependent: SubTables ToPrincipal: BaseTable ClientSetNull
    Indexes: 
      BaseTableBaseId, BaseTableBaseTypeId

Scaffolded code:

using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using test.Data.Models;

namespace test.Data;

public partial class TestDbContext : DbContext
{
    public TestDbContext(DbContextOptions<TestDbContext> options)
        : base(options)
    {
    }

    public virtual DbSet<BaseTable> BaseTables { get; set; }

    public virtual DbSet<SubTable> SubTables { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<BaseTable>(entity =>
        {
            entity.HasKey(e => e.BaseId).HasName("base_table_pkey");

            entity.Property(e => e.BaseId).ValueGeneratedNever();
        });

        modelBuilder.Entity<SubTable>(entity =>
        {
            entity.HasKey(e => e.SubId).HasName("sub_table_pkey");

            entity.Property(e => e.SubId).ValueGeneratedNever();

            entity.HasOne(d => d.BaseTable).WithMany(p => p.SubTables).HasPrincipalKey(p => new { p.BaseId, p.BaseTypeId });
        });

        OnModelCreatingPartial(modelBuilder);
    }

    partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;

namespace test.Data.Models;

[Table("base_table")]
[Index("BaseId", "BaseTypeId", Name = "AK_base_table_base_id_base_type_id", IsUnique = true)]
public partial class BaseTable
{
    [Key]
    [Column("base_id")]
    public long BaseId { get; set; }

    [Column("base_type_id")]
    public int BaseTypeId { get; set; }

    public virtual ICollection<SubTable> SubTables { get; } = new List<SubTable>();
}
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;

namespace test.Data.Models;

[Table("sub_table")]
public partial class SubTable
{
    [Key]
    [Column("sub_id")]
    public long SubId { get; set; }

    [Column("base_type_id")]
    public int BaseTypeId { get; set; }

    public virtual BaseTable BaseTable { get; set; } = null!;
}

Scaffolding command:

dotnet ef dbcontext scaffold "Data Source=(LocalDb)\MSSQLLocalDB;Database=AllTogetherNow" Microsoft.EntityFrameworkCore.SqlServer --no-onconfiguring --context TestDbContext --context-namespace test.Data --namespace test.Data.Models --data-annotations

To generate the test database:

public class SomeDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .UseSqlServer(@"Data Source=(LocalDb)\MSSQLLocalDB;Database=AllTogetherNow")
            .LogTo(Console.WriteLine, LogLevel.Information)
            .EnableSensitiveDataLogging();
}


public class Program
{
    public static async Task Main()
    {
        using (var context = new SomeDbContext())
        {
            await context.Database.EnsureDeletedAsync();
            await context.Database.EnsureCreatedAsync();

             context.Database.ExecuteSqlRaw(
                 @"      CREATE TABLE [dbo].[base_table] (
          [base_id] bigint NOT NULL,
          [base_type_id] int NOT NULL,
          CONSTRAINT [base_table_pkey] PRIMARY KEY ([base_id]),
          CONSTRAINT [AK_base_table_base_id_base_type_id] UNIQUE ([base_id], [base_type_id])
      );
");

             context.Database.ExecuteSqlRaw(
                 @"      CREATE TABLE [dbo].[sub_table] (
          [sub_id] bigint NOT NULL,
          [base_type_id] int NOT NULL,
          CONSTRAINT [sub_table_pkey] PRIMARY KEY ([sub_id]),
          CONSTRAINT [FK_sub_table_base_table_sub_id_base_type_id] FOREIGN KEY ([sub_id], [base_type_id]) REFERENCES [dbo].[base_table] ([base_id], [base_type_id]) ON DELETE CASCADE
      );
");
        }
    }
}

@scobei
Copy link
Author

scobei commented Dec 6, 2022

Adding HasForeignKey did the trick. Thanks!

@ajcvickers
Copy link
Contributor

Duplicate of #29418.

@ajcvickers ajcvickers closed this as not planned Won't fix, can't repro, duplicate, stale Dec 8, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants