SeaQL / SeaQL/sea-orm

sea-orm-cli generates incorrect relation for multi-column foreign key

Open
#2,662 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Area:code-gen
Dominant language
Rust
Stars
9.9k
Forks
735
Avg merge
6h 36m
Merged PRs (30d)
8

Description

Description

When a Postgres table has a foreign key constraint that includes more than one column, sea-orm-cli generates a relation with each column in the foreign key duplicated. This leads to sea-orm generating SQL joins that cannot possibly match any rows.

Steps to Reproduce

  1. Create a two-table schema where the second table references two columns in the first table.
  2. Run sea-orm-cli generate entity on the database.
  3. Observe that the Relation enum generated for the second table has the columns duplicated in the from and to bits in the sea_orm macro attribute.
  4. Try to do an inner join using sea-orm and see that the ON clause in the generated SQL has four equality constraints rather than two; two of them are correct, but the other two try to assert equality between non-corresponding columns.
Expected Behavior

sea-orm-cli generate entity should generate the Relation enum without repeating the columns multiple times.

Actual Behavior

Incorrect Relation generated leading to incorrect SQL join queries.

Reproduces How Often

Reproduces always

Workarounds

Manually editing the generated code can fix the problem.

Reproducible Example

I'll work up something more formal if needed, but here's a simple DB schema:

CREATE TABLE IF NOT EXISTS "first" (
    "auto_id" bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY NOT NULL,
    "a" varchar NOT NULL,
    "b" varchar NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS "idx_first_ab" ON "first" ("a", "b");

CREATE TABLE IF NOT EXISTS "second" (
    "auto_id" bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY NOT NULL,
    "f_a" varchar NOT NULL,
    "f_b" varchar NOT NULL,
    "c" varchar NOT NULL,
    CONSTRAINT "fk_ab" FOREIGN KEY ("f_a", "f_b") REFERENCES "first" ("a", "b") ON DELETE CASCADE
);

(The intent here is that there can be at most one row in first with the same a+b, and then there can be multiple rows in second with the same f_a+f_b that matches a+b in first, but with different values for c.)

This gives us the following as seen via psql:

                               Table "public.first"
 Column  |       Type        | Collation | Nullable |           Default            
---------+-------------------+-----------+----------+------------------------------
 auto_id | bigint            |           | not null | generated always as identity
 a       | character varying |           | not null | 
 b       | character varying |           | not null | 
Indexes:
    "first_pkey" PRIMARY KEY, btree (auto_id)
    "idx_first_ab" UNIQUE, btree (a, b)
Referenced by:
    TABLE "second" CONSTRAINT "fk_ab" FOREIGN KEY (f_a, f_b) REFERENCES first(a, b) ON DELETE CASCADE

                               Table "public.second"
 Column  |       Type        | Collation | Nullable |           Default            
---------+-------------------+-----------+----------+------------------------------
 auto_id | bigint            |           | not null | generated always as identity
 f_a     | character varying |           | not null | 
 f_b     | character varying |           | not null | 
 c       | character varying |           | not null | 
Indexes:
    "second_pkey" PRIMARY KEY, btree (auto_id)
Foreign-key constraints:
    "fk_ab" FOREIGN KEY (f_a, f_b) REFERENCES first(a, b) ON DELETE CASCADE

Running sea-orm-cli generate entity on this gives us the following inside second.rs:

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
    #[sea_orm(
        belongs_to = "super::first::Entity",
        from = "(Column::FA, Column::FA, Column::FB, Column::FB)",
        to = "(super::first::Column::A, super::first::Column::B, super::first::Column::A, super::first::Column::B)",
        on_update = "NoAction",
        on_delete = "Cascade"
    )]
    First,
}

Note the duplication in from and to. With sea-orm code like the following:

First::find()
    .inner_join(Second)
    .select_also(Second)
    .filter(...)

... we get a generated query with a join ON clause that looks like:

ON \
    "first"."a" = "second"."f_a" AND \
    "first"."b" = "second"."f_a" AND \
    "first"."a" = "second"."f_b" AND \
    "first"."b" = "second"."f_b"

The middle two equality bits will of course never work, and the query returns one row for each row in first, but with all of the columns in second empty.

If I then edit the generated code so the attributes on the macro instead read:

from = "(Column::FA, Column::FB",
to = "(super::first::Column::A, super::first::Column::B)",

... then I get generated queries with the correct join ON clause:

ON \
    "first"."a" = "second"."f_a" AND \
    "first"."b" = "second"."f_b"

... and I get back the rows I expect.

Versions

  • sea-bae v0.2.1
  • sea-orm v1.1.13
  • sea-orm-cli v1.1.13
  • sea-orm-macros v1.1.13
  • sea-orm-migration v1.1.13
  • sea-query v0.32.6
  • sea-query-derive v0.4.2
  • sea-query-binder v0.7.0
  • sea-schema v0.16.2
  • sea-schema-derive v0.3.0

OS: Debian trixie
Database: PostgreSQL 17.0 (via postgres:17-alpine Docker container)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Reproduce the issue with the supplied PostgreSQL schema using sea-orm-cli generate entity, then trace the entity-generation path that produces second.rs and its Relation enum. Compare the generated from and to attributes with the foreign-key column pairs and inspect the resulting join SQL. Done means multi-column foreign keys produce each pair once and joins contain only the corresponding equality constraints.

Written by the indexing model from the issue text.

Assessment

Tech stack
postgresql, rust
Domain
cli, database
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.