From 9040abf9ffe30f5a30e1f180277624661ab667b6 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 22 Sep 2026 13:24:23 -0700 Subject: [PATCH] fix(policy): mark relation subquery aliases as temp aliases so they get compacted Relation aliases were built as `${field}$${counter}` without the temp alias prefix, so the executor's `TempAliasTransformer` left them untouched. On PostgreSQL a relation field name of 62+ chars made every alias derived from it truncate to the same 63-byte identifier, producing either a "table name specified more than once" error or, across nested subqueries, the very self-relation shadowing this branch set out to fix. Route the aliases through `QueryUtils.tmpAlias()` so the existing transformer compacts them (or shortens them when they exceed the identifier limit if `useCompactAliasNames` is off). Add a PostgreSQL regression test with an over-long self many-to-many relation field. Co-Authored-By: Claude Fable 5.1 --- .../policy/src/expression-transformer.ts | 6 +++- tests/e2e/orm/policy/self-relation.test.ts | 30 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/packages/plugins/policy/src/expression-transformer.ts b/packages/plugins/policy/src/expression-transformer.ts index 0b1864a6f..9a3761365 100644 --- a/packages/plugins/policy/src/expression-transformer.ts +++ b/packages/plugins/policy/src/expression-transformer.ts @@ -477,9 +477,13 @@ export class ExpressionTransformer { * relation subquery avoids the related table shadowing an enclosing one when the relation points * back to the same model (self-relation), including across nested collection predicates. * The counter is per transformer instance so the same policy always compiles to the same SQL. + * + * The alias is marked as a temp alias so the query executor compacts it (or at least shortens + * it when it exceeds the database's identifier length limit), preventing PostgreSQL's 63-byte + * truncation from collapsing two aliases derived from a long field name into the same name. */ private newRelationAlias(field: string) { - return `${field}$${++this.aliasCounter}`; + return QueryUtils.tmpAlias(`${field}$${++this.aliasCounter}`); } private ensureCollectionPredicateOperator(op: BinaryOperator): asserts op is CollectionPredicateOperator { diff --git a/tests/e2e/orm/policy/self-relation.test.ts b/tests/e2e/orm/policy/self-relation.test.ts index 177db519f..0f8d4c1ea 100644 --- a/tests/e2e/orm/policy/self-relation.test.ts +++ b/tests/e2e/orm/policy/self-relation.test.ts @@ -210,4 +210,34 @@ model User { // 2 has friend 1 with same name -> readable; 3's friend 1 has a different name; 1 has no friends await expect(db.user.findMany()).resolves.toEqual([expect.objectContaining({ id: 2 })]); }); + + it('works with self relation whose field name exceeds postgres identifier length', async () => { + // 70 chars, well over PostgreSQL's 63-byte identifier limit. Without compaction, aliases + // derived from the name (e.g. `${name}$1` and `${name}$2`) would be truncated to the same + // identifier and the inner subquery would shadow the outer one again. + const field = 'friendsWithAVeryLongRelationFieldNameThatExceedsSixtyThreeBytesXXXXXXX'; + expect(field.length).toBeGreaterThan(63); + const db = await createPolicyTestClient( + ` +model User { + id Int @id + name String + ${field} User[] @relation("Friends") + friendOf User[] @relation("Friends") + + @@allow('create', true) + @@allow('read', ${field}?[name == this.name && ${field}?[name == this.name]]) +} +`, + { provider: 'postgresql', usePrismaPush: true, useCompactAliasNames: false }, + ); + const raw = db.$unuseAll(); + await raw.user.create({ data: { id: 1, name: 'x' } }); + await raw.user.create({ data: { id: 2, name: 'x', [field]: { connect: { id: 1 } } } }); + await raw.user.create({ data: { id: 3, name: 'x', [field]: { connect: { id: 2 } } } }); + await raw.user.create({ data: { id: 4, name: 'y', [field]: { connect: { id: 3 } } } }); + + // 3 -> 2 -> 1, all named 'x' -> readable; 2 -> 1 has no second hop; 4 differs in name + await expect(db.user.findMany()).resolves.toEqual([expect.objectContaining({ id: 3 })]); + }); });