payload icon indicating copy to clipboard operation
payload copied to clipboard

Relationship with custom text id not working when field is nested in layout components

Open lukabis opened this issue 4 months ago • 4 comments

Describe the Bug

Issue Description: Relationships fail when a custom text id field is nested inside layout components (like row) due to incorrect foreign key type generation.

Working Configuration: When the custom text id field is at the top level, relationships work correctly. Here's a repo where this issue is reproduced. The main branch works, while the issue branch demonstrates the error.

The repo contains 2 collections: Posts and Comments. Here are the working collection configurations:

export const Posts: CollectionConfig = {
  slug: 'posts',
  admin: {
    useAsTitle: 'title',
  },
  fields: [
    {
      name: 'id',
      required: true,
      type: 'text',
    },
    {
      name: 'title',
      type: 'text',
      required: true,
    },
  ],
};

export const Comments: CollectionConfig = {
  slug: 'comments',
  fields: [
    {
      name: 'text',
      type: 'textarea',
      required: true,
    },
    {
      name: 'post',
      type: 'relationship',
      relationTo: 'posts',
      required: true,
    },
  ],
};

The generated payload-types.ts has the expected type:

export interface Comment {
  id: number;
  text: string;
  post: string | Post; // string - correct
  updatedAt: string;
  createdAt: string;
}

Non-Working Configuration: When the custom id field is nested inside a row component, the relationship fails:

export const Posts: CollectionConfig = {
  slug: 'posts',
  admin: {
    useAsTitle: 'title',
  },
  fields: [
    {
      type: 'row',
      fields: [
        {
          name: 'id',
          required: true,
          type: 'text',
        },
        {
          name: 'title',
          type: 'text',
          required: true,
        },
      ],
    },
  ],
};


export const Comments: CollectionConfig = {
  slug: 'comments',
  fields: [
    {
      name: 'text',
      type: 'textarea',
      required: true,
    },
    {
      name: 'post',
      type: 'relationship',
      relationTo: 'posts',
      required: true,
    },
  ],
};

The generated payload-types.ts has an incorrect type:

export interface Comment {
  id: number;
  text: string;
  post: number | Post; // number - wrong
  updatedAt: string;
  createdAt: string;
}

Error:

Error: Failed query: ALTER TABLE "comments" ADD CONSTRAINT "comments_post_id_posts_id_fk" FOREIGN KEY ("post_id") REFERENCES "public"."posts"("id") ON DELETE set null ON UPDATE no action
detail: 'Key columns "post_id" and "id" are of incompatible types: integer and character varying.'

Root Cause: It appears that Payload's schema generation system doesn't properly detect custom text id fields when they're nested inside layout components like row, collapsible, or tabs. This causes the relationship system to default to number type instead of correctly using string to match the target field type.

Link to the code that reproduces this issue

https://github.com/lukabis/payload-custom-id-bug/tree/main

Reproduction Steps

  1. clone the repo
  2. cp .env.example .env
  3. git checkout issue
  4. docker compose up -d
  5. visit http://localhost:3000/admin
  6. check the logs docker compose logs -f payload

Which area(s) are affected? (Select all that apply)

db-postgres, area: core

Environment Info

This project is configured to use pnpm because /home/node/app/package.json has a "packageManager" field

Binaries:
  Node: 22.19.0
  npm: 10.9.3
  Yarn: N/A
  pnpm: 10.15.1
Relevant Packages:
  payload: 3.54.0
  next: 15.5.2
  @payloadcms/db-postgres: 3.54.0
  @payloadcms/email-nodemailer: 3.54.0
  @payloadcms/graphql: 3.54.0
  @payloadcms/next/utilities: 3.54.0
  @payloadcms/payload-cloud: 3.54.0
  @payloadcms/plugin-cloud-storage: 3.54.0
  @payloadcms/richtext-lexical: 3.54.0
  @payloadcms/storage-s3: 3.54.0
  @payloadcms/translations: 3.54.0
  @payloadcms/ui/shared: 3.54.0
  react: 19.1.1
  react-dom: 19.1.1
Operating System:
  Platform: linux
  Arch: x64
  Version: #79~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Fri Aug 15 16:54:53 UTC 2
  Available memory (MB): 15715
  Available CPU cores: 16

lukabis avatar Sep 04 '25 17:09 lukabis

I think this problem is related to this issue.

danielkoller avatar Sep 08 '25 19:09 danielkoller

@danielkoller I can't see the two issues are related.

I can confirm this issue exists and affects all custom text ID fields, not just those in row fields.

Evidence from generated schema:

When using custom text ID fields, Payload correctly generates the main collection table with varchar ID:

export const posts = pgTable('posts', {
  id: varchar('id').primaryKey(),
  // ...
})

However, relationship tables still use integer for foreign key columns:

export const comments_rels = pgTable('comments_rels', {
  id: serial('id').primaryKey(),
  postsID: integer('posts_id'),  // ❌ Should be varchar
  // ...
})

This creates foreign key constraints that try to link incompatible types:

postsIdFk: foreignKey({
  columns: [columns['postsID']],     // integer type
  foreignColumns: [posts.id],       // varchar type
  name: 'comments_rels_posts_fk',
}).onDelete('cascade'),

Error:

Key columns "posts_id" and "id" are of incompatible types: integer and character varying.

This makes the Custom text ID Fields completely unusable with PostgreSQL when any relationships exist between collections.

Environment:

  • Payload: 3.50.0
  • Database: PostgreSQL 17-3.5

nathanbowang avatar Sep 23 '25 22:09 nathanbowang

I created a tiny collection (just id + one text field), and it works fine.
In my real project, collections that contains a relationship array, or join will fail. At certain points Payload appears to treat my id (defined as text) as an integer.

My workaround is use a new filed for the text ids.

nathanbowang avatar Sep 24 '25 01:09 nathanbowang

I just experienced this exact bug with the group field type. I was using it purely for layout purposes with a custom text id field nested inside.

This is a subtle bug because there's no warning during config validation. My current workaround is to move the custom id field to the top level, which defeats the purpose of layout organization. While I'm not sure if this happens to other databases, it might be related to drizzle or postgres specifically.

Additionally while we fix this issue, it would be helpful to add a warning callout in the docs so developers are aware of this caveat. I could also contribute a PR to add this documentation, however I need to confirm if this only happens to Postgres.

junwen-k avatar Nov 22 '25 15:11 junwen-k