sea-orm-cli generates incorrect relation for multi-column foreign key
Nobody has claimed this yet.
- 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
- Create a two-table schema where the second table references two columns in the first table.
- Run
sea-orm-cli generate entityon the database. - Observe that the
Relationenum generated for the second table has the columns duplicated in thefromandtobits in thesea_ormmacro attribute. - Try to do an inner join using sea-orm and see that the
ONclause 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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