Skip to content

✨ enhance the schema.rb parser to support Constraints #1408

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

Open
wants to merge 13 commits into
base: main
Choose a base branch
from

Conversation

tnyo43
Copy link
Member

@tnyo43 tnyo43 commented Apr 20, 2025

Issue

  • resolve:

Why is this change needed?

We introduced constraints of schema and update tbls and Prisma parser to support the feature.
This PR will update schema.rb parser to support constraints.

ref

What would you like reviewers to focus on?

  • the constraints are parsed properly
    • The schema.rb parser will support "PRIMARY KEY", "FOREIGN KEY", "UNIQUE" and "CHECK" constraints.
  • the tests are appropriate

Testing Verification

  • About the parser, I updated some test cases
  • About the application, I checked the behaviour in the following steps
    1. copy the following schema and save on your local PC (that's a slight modified copy of this file)
    2. run pnpm run build --filter @liam-hq/cli to build cli
    3. run node frontend/packages/cli/dist-cli/bin/cli.js erd build --input schema.rb to generate scripts
    4. run npx http-server dist to run the application and go to http://localhost:8080/
    5. click "users" table and see the Constraints section of its table details

Screenshot 0007-04-22 at 21 18 12

schema.rb
create_table "companies", id: :uuid, default: -> { "gen_random_uuid()" }, comment: "Represents organizations using the system. Each company is a top-level entity that owns departments, users, and projects.", force: :cascade do |t|
  t.string "name", null: false, comment: "The name of the company."
  t.string "address", null: false, comment: "The registered address of the company."
end

create_table "departments", id: :uuid, default: -> { "gen_random_uuid()" }, comment: "Represents departments within a company, organizing users into functional groups.", force: :cascade do |t|
  t.string "name", null: false, comment: "The name of the department."
  t.uuid "company_id", null: false, comment: "Foreign key linking the department to a company."
  t.index ["company_id"], name: "index_departments_on_company_id"
end

create_table "users", id: :uuid, default: -> { "gen_random_uuid()" }, comment: "Represents employees or members of a company, who are assigned roles and tasks.", force: :cascade do |t|
  t.uuid "company_id", null: false, comment: "Foreign key linking the user to a company."
  t.uuid "department_id", null: false, comment: "Foreign key linking the user to a department."
  t.datetime "created_at", null: false, comment: "The timestamp when the user was created."
  t.string "name", default: "new user", null: true, comment: "The user's full name."
  t.string "email", null: false, comment: "The user's email address."
  t.integer "age", default: 30, null: true, comment: "The user's age."
  t.boolean "is_deleted", default: false, null: true, comment: "Indicates whether the user is deleted (soft delete)."
  t.string "user_name", null: false, comment: "A unique identifier or login name for the user."
  t.index ["company_id"], name: "index_users_on_company_id"
  t.index ["department_id"], name: "index_users_on_department_id"
  t.index ["email"], name: "index_users_on_email", unique: true
end

create_table "roles", id: :uuid, default: -> { "gen_random_uuid()" }, comment: "Defines roles that can be assigned to users, such as 'Admin' or 'Manager'.", force: :cascade do |t|
  t.string "name", null: false, comment: "The name of the role."
  t.string "description", comment: "A brief description of the role's purpose or permissions."
end

create_table "user_roles", id: :uuid, default: -> { "gen_random_uuid()" }, comment: "Associates users with roles to define their permissions within the company.", force: :cascade do |t|
  t.uuid "user_id", null: false, comment: "Foreign key linking to the user."
  t.uuid "role_id", null: false, comment: "Foreign key linking to the role."
  t.index ["user_id", "role_id"], unique: true, name: "index_user_roles_on_user_id_and_role_id", comment: "Ensures that each user-role pair is unique."
end

create_table "projects", id: :uuid, default: -> { "gen_random_uuid()" }, comment: "Represents projects managed within a company. Projects are linked to tasks and users.", force: :cascade do |t|
  t.string "name", null: false, comment: "The name of the project."
  t.text "description", comment: "A detailed description of the project."
  t.uuid "company_id", null: false, comment: "Foreign key linking the project to a company."
  t.index ["company_id"], name: "index_projects_on_company_id"
end

create_table "project_assignments", id: :uuid, default: -> { "gen_random_uuid()" }, comment: "Associates users with projects they are assigned to work on.", force: :cascade do |t|
  t.uuid "user_id", null: false, comment: "Foreign key linking to the user."
  t.uuid "project_id", null: false, comment: "Foreign key linking to the project."
  t.index ["user_id", "project_id"], unique: true, name: "index_project_assignments_on_user_id_and_project_id", comment: "Ensures that each user is uniquely assigned to a project."
end

create_table "tasks", id: :uuid, default: -> { "gen_random_uuid()" }, comment: "Represents tasks within a project, assigned to users with deadlines and statuses.", force: :cascade do |t|
  t.string "title", null: false, comment: "The title of the task."
  t.text "description", comment: "A detailed description of the task."
  t.uuid "project_id", null: false, comment: "Foreign key linking the task to a project."
  t.uuid "assigned_user_id", null: false, comment: "Foreign key linking the task to the assigned user."
  t.datetime "due_date", comment: "The deadline for completing the task."
  t.integer "status", default: 0, comment: "The current status of the task (e.g., 0: pending, 1: in progress, 2: completed)."
  t.index ["project_id"], name: "index_tasks_on_project_id"
  t.index ["assigned_user_id"], name: "index_tasks_on_assigned_user_id"
end

create_table "comments", id: :uuid, default: -> { "gen_random_uuid()" }, comment: "Stores comments on tasks, enabling discussions or updates.", force: :cascade do |t|
  t.text "content", null: false, comment: "The content of the comment."
  t.uuid "task_id", null: false, comment: "Foreign key linking the comment to a task."
  t.uuid "user_id", null: false, comment: "Foreign key linking the comment to the user who wrote it."
  t.datetime "created_at", null: false, comment: "The timestamp when the comment was created."
  t.index ["task_id"], name: "index_comments_on_task_id"
  t.index ["user_id"], name: "index_comments_on_user_id"
end

create_table "timesheets", id: :uuid, default: -> { "gen_random_uuid()" }, comment: "Tracks time spent by users on tasks for reporting or billing purposes.", force: :cascade do |t|
  t.uuid "user_id", null: false, comment: "Foreign key linking the timesheet to a user."
  t.uuid "task_id", null: false, comment: "Foreign key linking the timesheet to a task."
  t.datetime "start_time", null: false, comment: "The timestamp when the user started working on the task."
  t.datetime "end_time", comment: "The timestamp when the user stopped working on the task."
  t.integer "duration_minutes", null: false, comment: "The total duration of work in minutes."
  t.index ["user_id"], name: "index_timesheets_on_user_id"
  t.index ["task_id"], name: "index_timesheets_on_task_id"
end

add_foreign_key "departments", "companies", column: "company_id", name: "fk_departments_company_id", on_update: :restrict, on_delete: :cascade
add_foreign_key "users", "companies", column: "company_id", name: "fk_users_company_id", on_update: :restrict, on_delete: :cascade
add_foreign_key "users", "departments", column: "department_id", name: "fk_users_department_id", on_update: :restrict, on_delete: :cascade
add_foreign_key "user_roles", "users", column: "user_id", name: "fk_user_roles_user_id", on_update: :restrict, on_delete: :cascade
add_foreign_key "user_roles", "roles", column: "role_id", name: "fk_user_roles_role_id", on_update: :restrict, on_delete: :cascade
add_foreign_key "projects", "companies", column: "company_id", name: "fk_projects_company_id", on_update: :restrict, on_delete: :cascade
add_foreign_key "project_assignments", "users", column: "user_id", name: "fk_project_assignments_user_id", on_update: :restrict, on_delete: :cascade
add_foreign_key "project_assignments", "projects", column: "project_id", name: "fk_project_assignments_project_id", on_update: :restrict, on_delete: :cascade
add_foreign_key "tasks", "projects", column: "project_id", name: "fk_tasks_project_id", on_update: :restrict, on_delete: :cascade
add_foreign_key "tasks", "users", column: "assigned_user_id", name: "fk_tasks_assigned_user_id", on_update: :restrict, on_delete: :restrict
add_foreign_key "comments", "tasks", column: "task_id", name: "fk_comments_task_id", on_update: :restrict, on_delete: :cascade
add_foreign_key "comments", "users", column: "user_id", name: "fk_comments_user_id", on_update: :restrict, on_delete: :cascade
add_foreign_key "timesheets", "users", column: "user_id", name: "fk_timesheets_user_id", on_update: :restrict, on_delete: :cascade
add_foreign_key "timesheets", "tasks", column: "task_id", name: "fk_timesheets_task_id", on_update: :restrict, on_delete: :cascade
add_check_constraint "users", "age >= 0 and age < 20", name: "age_range_check"

What was done

🤖 Generated by PR Agent at 8d71055

  • Add support for parsing constraints in the schema.rb parser
    • Handle PRIMARY KEY, FOREIGN KEY, UNIQUE, and CHECK constraints
    • Store constraints in the table schema structure
  • Update and expand test cases to verify constraint parsing
    • Add tests for unique indexes, foreign keys, and check constraints
  • Add factory functions for constraint objects in schema utilities
  • Update schema exports to include new constraint types and factories

Detailed Changes

Relevant files
Enhancement
parser.ts
Add constraint extraction and storage to schema.rb parser

frontend/packages/db-structure/src/parser/schemarb/parser.ts

  • Add logic to extract and store PRIMARY KEY, FOREIGN KEY, UNIQUE, and
    CHECK constraints
  • Update table creation to include constraints in schema
  • Implement extraction of check constraints and foreign key options
  • Store constraints in table objects and handle relevant parser methods
  • +134/-13
    factories.ts
    Add constraint factories for schema objects                           

    frontend/packages/db-structure/src/schema/factories.ts

  • Add factory functions for PRIMARY KEY, FOREIGN KEY, UNIQUE, and CHECK
    constraints
  • Export new constraint factories for use in tests and parser
  • +44/-0   
    index.ts
    Export constraint types and factories from schema module 

    frontend/packages/db-structure/src/schema/index.ts

  • Export new constraint types and factories (PRIMARY KEY, FOREIGN KEY,
    UNIQUE, CHECK)
  • Update exports to support new constraint handling in parser and tests
  • +5/-0     
    Tests
    index.test.ts
    Add and update tests for constraint parsing                           

    frontend/packages/db-structure/src/parser/schemarb/index.test.ts

  • Expand tests to verify parsing of constraints (PRIMARY KEY, FOREIGN
    KEY, UNIQUE, CHECK)
  • Update expected schema objects to include constraints
  • Add new test cases for check constraints and constraint details
  • +171/-30
    Documentation
    wet-zoos-add.md
    Add changeset for schema.rb constraint support                     

    .changeset/wet-zoos-add.md

  • Add changeset entry describing enhancement of schema.rb parser for
    constraints
  • +5/-0     

    Additional Notes


    Need help?
  • Type /help how to ... in the comments thread for any questions about Qodo Merge usage.
  • Check out the documentation for more information.
  • Copy link

    changeset-bot bot commented Apr 20, 2025

    🦋 Changeset detected

    Latest commit: 4ac6aa3

    The changes in this PR will be included in the next version bump.

    This PR includes changesets to release 2 packages
    Name Type
    @liam-hq/db-structure Patch
    @liam-hq/cli Patch

    Not sure what this means? Click here to learn what changesets are.

    Click here if you're a maintainer who wants to add another changeset to this PR

    Copy link

    vercel bot commented Apr 20, 2025

    The latest updates on your projects. Learn more about Vercel for Git ↗︎

    Name Status Preview Comments Updated (UTC)
    liam-app ✅ Ready (Inspect) Visit Preview 💬 Add feedback Apr 26, 2025 10:19am
    liam-docs ✅ Ready (Inspect) Visit Preview 💬 Add feedback Apr 26, 2025 10:19am
    liam-erd-sample ✅ Ready (Inspect) Visit Preview 💬 Add feedback Apr 26, 2025 10:19am

    Copy link

    supabase bot commented Apr 20, 2025

    Updates to Preview Branch (feat/schemarb-constraints) ↗︎

    Deployments Status Updated
    Database Sat, 26 Apr 2025 10:15:58 UTC
    Services Sat, 26 Apr 2025 10:15:58 UTC
    APIs Sat, 26 Apr 2025 10:15:58 UTC

    Tasks are run on every commit but only new migration files are pushed.
    Close and reopen this PR if you want to apply changes from existing seed or migration files.

    Tasks Status Updated
    Configurations Sat, 26 Apr 2025 10:16:00 UTC
    Migrations Sat, 26 Apr 2025 10:16:01 UTC
    Seeding Sat, 26 Apr 2025 10:16:01 UTC
    Edge Functions Sat, 26 Apr 2025 10:16:01 UTC

    View logs for this Workflow Run ↗︎.
    Learn more about Supabase for Git ↗︎.

    This comment was marked as resolved.

    Comment on lines +111 to +115
    const idPrimaryKeyConstraint: PrimaryKeyConstraint = {
    type: 'PRIMARY KEY',
    name: 'PRIMARY_id',
    columnName: 'id',
    }
    Copy link
    Member Author

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    It generates PRIMARY KEY constraint object at the same time of it generates id column.

    Comment on lines +161 to +168
    if (index.unique && index.columns[0]) {
    const uniqueConstraint: UniqueConstraint = {
    type: 'UNIQUE',
    name: `UNIQUE_${index.columns[0]}`,
    columnName: index.columns[0],
    }
    constraints.push(uniqueConstraint)
    }
    Copy link
    Member Author

    @tnyo43 tnyo43 Apr 20, 2025

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    It will set UNIQUE constraints when unique index objects is created.
    https://api.rubyonrails.org/v8.0.2/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_index

    I understand this is the only way to set unique constraints on the database.

    In order to add a uniqueness database constraint on your database, use the add_index statement in a migration and include the unique: true option.
    https://guides.rubyonrails.org/active_record_validations.html#:~:text=In%20order%20to%20add%20a%20uniqueness%20database%20constraint%20on%20your%20database%2C%20use%20the%20add_index%20statement%20in%20a%20migration%20and%20include%20the%20unique%3A%20true%20option.

    You can see how it works in index (unique: true) test case.

    Copy link
    Member

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    I have the same understanding!👍

    @@ -278,6 +309,51 @@ function extractRelationshipTableNames(
    return ok([primaryTableName, foreignTableName])
    }

    function extractCheckConstraint(
    Copy link
    Member Author

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Here is a function to get check constraint from add_check_constraint statement.

    It will extract the table name, detail and constraint name from the statement.

    @tnyo43
    Copy link
    Member Author

    tnyo43 commented Apr 20, 2025

    Oops, it looks createParserTestCases function is shared among the parser tests 👀
    So it will cause something wrong if I change the object for a single test.
    https://github.com/liam-hq/liam/actions/runs/14557016240/job/40835263403?pr=1408

    However, I'm not sure if all the result must be the same. For example, index (unique: true) will make the target column be unique for the schame.rb parser, but not for the tbls parser, since it gets constraints from schema's constraints section.

    tnyo43 added 4 commits April 22, 2025 20:46
    The expected objects of the test cases of schama.rb's parser are different than the other parsers, especially than the tbls's parser. They will requires their own test results.
    })

    it('foreign key (one-to-many)', async () => {
    it('foreign key', async () => {
    Copy link
    Member Author

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    I created a test of "foreign key" to test the behaviour with add_foreign_key by checking both relationships and constraints.
    Since the existing cases like "foreign key (one-to-many)" were intended to test relationship cardinalities, I grouped them under a describe block named "foreign key cardinality". (I think using "Hide whitespace" to will make the diff easier to read)

    @tnyo43 tnyo43 marked this pull request as ready for review April 22, 2025 12:23
    @tnyo43 tnyo43 requested a review from a team as a code owner April 22, 2025 12:23
    @tnyo43 tnyo43 requested review from hoshinotsuyoshi, FunamaYukina, junkisai, MH4GF and NoritakaIkeda and removed request for a team April 22, 2025 12:23
    Copy link
    Contributor

    PR Reviewer Guide 🔍

    Here are some key observations to aid the review process:

    ⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
    🧪 PR contains tests
    🔒 No security concerns identified
    ⚡ Recommended focus areas for review

    Constraint Naming

    The unique constraint naming convention uses a hardcoded prefix "UNIQUE_" followed by the column name. This approach might lead to naming conflicts if multiple unique constraints exist on the same column or if the constraint is on multiple columns.

    const uniqueConstraint: UniqueConstraint = {
      type: 'UNIQUE',
      name: `UNIQUE_${index.columns[0]}`,
      columnName: index.columns[0],
    Error Handling

    The code doesn't handle the case where a foreign key constraint references a table that doesn't exist yet. This could lead to constraints not being properly added to tables that are defined after their references.

      const foreignTable = this.tables.find(
        (table) => table.name === foreignTableName,
      )
      if (foreignTable) {
        foreignTable.constraints[foreignKeyConstraint.name] = foreignKeyConstraint
      }
    }

    Copy link
    Contributor

    qodo-merge-pro-for-open-source bot commented Apr 22, 2025

    PR Code Suggestions ✨

    Explore these optional code suggestions:

    CategorySuggestion                                                                                                                                    Impact
    Possible issue
    Fix multi-column unique constraints

    The unique constraint is being created with a hardcoded name pattern that
    doesn't handle multi-column indexes correctly. For multi-column indexes, the
    constraint should include all column names or use the index name to ensure
    uniqueness.

    frontend/packages/db-structure/src/parser/schemarb/parser.ts [143-168]

     function extractTableDetails(
       blockNodes: Node[],
     ): [Column[], Index[], Constraint[]] {
       const columns: Column[] = []
       const indexes: Index[] = []
       const constraints: Constraint[] = []
     
       for (const blockNode of blockNodes) {
         if (blockNode instanceof StatementsNode) {
           for (const node of blockNode.compactChildNodes()) {
             if (
               node instanceof CallNode &&
               node.receiver instanceof LocalVariableReadNode &&
               node.receiver.name === 't'
             ) {
               if (node.name === 'index') {
                 const index = extractIndexDetails(node)
                 indexes.push(index)
    -            if (index.unique && index.columns[0]) {
    +            if (index.unique && index.columns.length > 0) {
                   const uniqueConstraint: UniqueConstraint = {
                     type: 'UNIQUE',
    -                name: `UNIQUE_${index.columns[0]}`,
    +                name: `UNIQUE_${index.name || index.columns.join('_')}`,
                     columnName: index.columns[0],
    +                columns: index.columns,
                   }
                   constraints.push(uniqueConstraint)
                 }
                 continue
               }

    [To ensure code accuracy, apply this suggestion manually]

    Suggestion importance[1-10]: 8

    __

    Why: The current implementation doesn't properly handle multi-column unique indexes, as it only uses the first column for the constraint name and doesn't store all columns. The improved code fixes this by using either the index name or joining all column names for the constraint name, and storing all columns in the constraint.

    Medium
    Fix invalid check constraint

    The test case contains a logically impossible check constraint condition: "age
    >= 20 and age < 20". This condition can never be true and would prevent any rows
    from being inserted. Use a valid condition like "age >= 20 and age < 100"
    instead.

    frontend/packages/db-structure/src/parser/schemarb/parser.ts [395-413]

     it('check constraint', async () => {
       const { value } = await processor(/* Ruby */ `
         create_table "users" do |t|
           t.integer "age"
         end
     
    -    add_check_constraint "users", "age >= 20 and age < 20", name: "age_range_check"
    +    add_check_constraint "users", "age >= 20 and age < 100", name: "age_range_check"
       `)
     
       expect(value.tables['users']?.constraints).toEqual({
         PRIMARY_id: aPrimaryKeyConstraint({
           name: 'PRIMARY_id',
           columnName: 'id',
         }),
         age_range_check: aCheckConstraint({
           name: 'age_range_check',
    -      detail: 'age >= 20 and age < 20',
    +      detail: 'age >= 20 and age < 100',
         }),
       })
     })

    [To ensure code accuracy, apply this suggestion manually]

    Suggestion importance[1-10]: 7

    __

    Why: The test case contains a logically impossible check constraint condition ("age >= 20 and age < 20") that would prevent any rows from being inserted. The fix replaces it with a valid condition ("age >= 20 and age < 100") that makes logical sense.

    Medium
    Learned
    best practice
    Validate that extracted string values are non-empty before using them in critical operations

    Add validation to ensure that both tableName and detail are non-empty strings
    before proceeding with constraint creation. Empty strings could lead to invalid
    constraints being created.

    frontend/packages/db-structure/src/parser/schemarb/parser.ts [312-330]

     function extractCheckConstraint(
       argNodes: Node[],
     ): Result<
       { tableName: string; constraint: CheckConstraint },
       UnexpectedTokenWarningError
     > {
       const stringNodes = argNodes.filter((node) => node instanceof StringNode)
       if (stringNodes.length !== 2) {
         return err(
           new UnexpectedTokenWarningError(
             'Check constraint must have one table name and its detail',
           ),
         )
       }
     
       const [tableName, detail] = stringNodes.map((node): string => {
         if (node instanceof StringNode) return node.unescaped.value
         return ''
       }) as [string, string]
    +  
    +  if (!tableName || !detail) {
    +    return err(
    +      new UnexpectedTokenWarningError(
    +        'Check constraint requires non-empty table name and detail',
    +      ),
    +    )
    +  }
     }

    [To ensure code accuracy, apply this suggestion manually]

    Suggestion importance[1-10]: 6
    Low
    • Update

    Comment on lines +161 to +165
    if (index.unique && index.columns[0]) {
    const uniqueConstraint: UniqueConstraint = {
    type: 'UNIQUE',
    name: `UNIQUE_${index.columns[0]}`,
    columnName: index.columns[0],
    Copy link
    Member

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Just a quick question for clarification!
    Are we extracting only the first item from the columns array here because of the reasoning mentioned in this comment?

    #1168 (comment)

    Copy link
    Member Author

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    extracting only the first item from the columns array here because of the reasoning mentioned in this comment

    Yes, and same for the other parsers as well (e.g. this is an implementation of tbls's Unique constraints)

    Comment on lines +566 to +567
    if (node.name === 'add_check_constraint')
    this.handleAddCheckConstraint(node)
    Copy link
    Member

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    In recent versions of Rails (7.1 and later) and with the mysql2 adapter, t.check_constraint is now supported and will be included in schema.rb.👀
    like this:

    create_table "quiz_choices", force: :cascade do |t|
      t.integer "display_order", default: 0, null: false
      t.check_constraint "`display_order` between 1 and 4", name: "display_order_check"
    end
    

    ref: https://kazuya-engineer.com/2024/02/03/rails-migrations-allow-specific-values/

    If it is to be supported, it would be in the form of adding it to extractTableDetails function.💭
    I don't think we must necessarily address this in this pull request!

    Copy link
    Member Author

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Thanks, I missed the method.
    Yes, it seems necessary to support, but maybe in a separate PR. I'll create an issue later.

    Copy link
    Member

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Thank you so much! ✨

    Copy link
    Member

    @FunamaYukina FunamaYukina left a comment

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    I commented on one, but the others are LGTM! Thank you!🌟

    @@ -311,25 +388,31 @@ function extractForeignKeyOptions(
    case 'column':
    if (value instanceof StringNode || value instanceof SymbolNode) {
    relation.foreignColumnName = value.unescaped.value
    foreignKeyConstraint.columnName = value.unescaped.value
    Copy link
    Member

    @FunamaYukina FunamaYukina Apr 25, 2025

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    (sorry, I wasn't able to submit my comment.)
    In lines 391, 397, and 406, it would be better to set “foreignKeyConstraint.name” as well. The name seemed to remain empty.

    ss 3201

    like this?💭

    case 'column'
    foreignKeyConstraint.name = `fk_${value.unescaped.value}`
    
    case 'on_update'
    foreignKeyConstraint.name = `on_update_${updateConstraint}`
    
    case 'on_delete'
    foreignKeyConstraint.name = `on_delete_${updateConstraint}`
    

    Copy link
    Member Author

    @tnyo43 tnyo43 Apr 26, 2025

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Thanks for pointing it out, but unfortunately I can't reproduce the problem on my end.
    Could you please share the schema you used? That would help me understand the problem more easily.

    It seemds like the add_foreign_key method always provides a name property, even if it isn't explicitly set, so I guess there is no need toadd extra lines for it 🤔
    In any cases, a concrete example will be super helpful 😉

    :name
    The constraint name. Defaults to fk_rails_<identifier>.
    https://api.rubyonrails.org/v4.2.0/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_foreign_key

    Copy link
    Member Author

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    📝 To reproduce, visit https://liam-app-git-feat-schemarb-constraints-route-06-core.vercel.app/erd/p/github.com/mastodon/mastodon/blob/1bc28709ccde4106ab7d654ad5888a14c6bb1724/db/schema.rb?active=account_aliases

    The constraint name should be fk_rails_<identifier> and the identifier is a 10 character long random HEX string if the name is not provided (please refer to this).
    I created a random HEX string generator and set the constraint name and column name if not provided explicitly in this commit 83cc5e5
    Thanks for your kind review 👍

    Comment on lines 424 to 440
    if (relation.foreignColumnName === '') {
    relation.foreignColumnName = `${singularize(relation.primaryTableName)}_id`
    const columnName = `${singularize(relation.primaryTableName)}_id`
    relation.foreignColumnName = columnName
    foreignKeyConstraint.columnName = columnName
    }

    if (relation.name === '') {
    relation.name = defaultRelationshipName(
    const relationshipName = defaultRelationshipName(
    relation.primaryTableName,
    relation.primaryColumnName,
    relation.foreignTableName,
    relation.foreignColumnName,
    )
    relation.name = relationshipName
    foreignKeyConstraint.name = relationshipName
    }
    }
    Copy link
    Member Author

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    I set the same values for the constraint's name and columnName as those of the relationship's name and columnName if they are not specified explicitly.
    Although that name do not strictly follow the add_foreign_key specification (fk_rails_<identifier>. identifier is a 10 character long random string), I chose to assign these values because reproducing the exact specification would make the implementation overly complex.

    I initially tried implementing the specification-compliant names (please check this commit if you are interested in), but the implementation became too complicated, so I decided not to proceed with it.

    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
    Projects
    None yet
    Development

    Successfully merging this pull request may close these issues.

    3 participants