Skip to content
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

Feat/support for many to many relationship #1038

Closed

Conversation

prakha
Copy link
Contributor

@prakha prakha commented Mar 29, 2025

Issue

  • resolve:

Why is this change needed?

before there was. no support for many to many table. So i have added the logic that will generate the implicit table when many-to-many exist between the table.

What would you like reviewers to focus on?

Testing Verification

What was done

🤖 Generated by PR Agent at 8023912

  • Added support for many-to-many relationships in schema parsing.
  • Implemented logic to generate implicit join tables for many-to-many associations.
  • Updated cardinality schema to include MANY_TO_MANY.
  • Removed many-to-many relationships from the relationships object post-processing.

Detailed Changes

Relevant files
Enhancement
parser.ts
Enhance schema parser to support many-to-many relationships

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

  • Added logic to identify and handle many-to-many relationships.
  • Implemented generation of implicit join tables for many-to-many
    associations.
  • Updated relationship cardinality handling to include MANY_TO_MANY.
  • Removed many-to-many relationships from the relationships object after
    processing.
  • +163/-23
    dbStructure.ts
    Update cardinality schema to include MANY_TO_MANY               

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

  • Added MANY_TO_MANY to the cardinality schema.
  • Updated type definitions to reflect the new cardinality.
  • +5/-1     

    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.
  • @prakha prakha requested a review from a team as a code owner March 29, 2025 22:55
    @prakha prakha requested review from hoshinotsuyoshi, FunamaYukina, junkisai, MH4GF and NoritakaIkeda and removed request for a team March 29, 2025 22:55
    Copy link

    changeset-bot bot commented Mar 29, 2025

    ⚠️ No Changeset found

    Latest commit: 5a92865

    Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

    This PR includes no changesets

    When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

    Click here to learn what changesets are, and how to add one.

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

    Copy link

    vercel bot commented Mar 29, 2025

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

    3 Skipped Deployments
    Name Status Preview Comments Updated (UTC)
    liam-docs ⬜️ Ignored (Inspect) Visit Preview Apr 1, 2025 0:57am
    test-liam-docs ⬜️ Ignored (Inspect) Apr 1, 2025 0:57am
    test-liam-erd-sample ⬜️ Ignored (Inspect) Apr 1, 2025 0:57am

    Copy link

    vercel bot commented Mar 29, 2025

    @prakha is attempting to deploy a commit to the ROUTE06 Core Team on Vercel.

    A member of the Team first needs to authorize it.

    Copy link
    Contributor

    PR Reviewer Guide 🔍

    Here are some key observations to aid the review process:

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

    Possible Issue

    The logic for determining cardinality in the relationship object may not handle all edge cases correctly, especially when transitioning between ONE_TO_ONE, ONE_TO_MANY, and MANY_TO_MANY. This should be carefully reviewed to ensure correctness.

    if (field.isList) {
      if (
        existingRelationship &&
        existingRelationship.cardinality === 'ONE_TO_ONE'
      ) {
        // If an existing relation is ONE_TO_ONE and this field is a list → Change it to ONE_TO_MANY
        relationship.cardinality = 'ONE_TO_MANY'
      } else {
        // Otherwise, it's a MANY_TO_MANY relationship
        relationship.cardinality = 'MANY_TO_MANY'
      }
    } else {
      if (
        existingRelationship &&
        existingRelationship.cardinality === 'MANY_TO_MANY' &&
        isTargetField
      ) {
        relationship.cardinality = 'ONE_TO_MANY'
      }
    }
    Performance Concern

    The processTableIndices function iterates over indices and fields, potentially leading to performance issues for large schemas. Consider optimizing or limiting the scope of this function.

    function processTableIndices(
      indices: readonly DMMF.Index[],
      tableName: string,
      tables: Record<string, Table>,
      columns: Columns,
      relationshipName: string,
      relationships: Record<string, Relationship>,
      relationshipTableName: string,
    ): void {
      for (const table of indices) {
        for (const field of table.fields) {
          const existingColumns = tableName && tables[tableName]?.columns['id']
          const columnName = `${table.model}_${field.name}`
          if (existingColumns && typeof existingColumns === 'object') {
            columns[columnName] = {
              name: columnName,
              type: existingColumns.type ?? 'id',
              default: existingColumns.default ?? '',
              notNull: existingColumns.notNull,
              unique: existingColumns.unique,
              primary: existingColumns.primary,
              comment: existingColumns.comment,
              check: existingColumns.check,
            }
          } else {
            columns[columnName] = {
              name: columnName,
              type: 'id',
              default: '',
              notNull: false,
              unique: false,
              primary: false,
              comment: '',
              check: '',
            }
          }
    
          const relationShipWithTheTable = `${table.model}To${relationshipName}`
          relationships[relationShipWithTheTable] = {
            name: relationShipWithTheTable,
            primaryTableName: table.model,
            primaryColumnName: field?.name,
            foreignTableName: relationshipTableName,
            foreignColumnName: columnName,
            cardinality: 'ONE_TO_MANY',
            updateConstraint: 'CASCADE',
            deleteConstraint: 'CASCADE',
          }
        }
      }
    }
    Code Clarity

    The logic for generating and processing many-to-many relationships is complex and spans multiple functions. It may benefit from additional comments or refactoring for better readability and maintainability.

    const manyToManyRelationships = Object.fromEntries(
      Object.entries(relationships).filter(
        ([_, rel]) => rel.cardinality === 'MANY_TO_MANY',
      ),
    )
    
    if (Object.keys(manyToManyRelationships).length) {
      for (const relationship in manyToManyRelationships) {
        const relationshipValue = manyToManyRelationships[relationship]
        if (!relationshipValue) continue // Skip if undefined
        const relationshipName = relationshipValue?.name
    
        const { primaryTableName, foreignTableName } = relationshipValue
        const indexes = dmmf?.datamodel?.indexes
        if (!indexes) continue
    
        const primaryTableIndices = getTableIndices(indexes, primaryTableName)
    
        const foreignTableIndices = getTableIndices(indexes, foreignTableName)
    
        const columns: Columns = {}
    
        const relationshipTableName = `_${relationship}`
        processTableIndices(
          primaryTableIndices,
          primaryTableName,
          tables,
          columns,
          relationshipName,
          relationships,
          relationshipTableName,
        )
        processTableIndices(
          foreignTableIndices,
          foreignTableName,
          tables,
          columns,
          relationshipName,
          relationships,
          relationshipTableName,
        )
    
        const indicesColumn = Object.keys(columns)
        const indicesName = `${relationshipValue?.name}_pkey`
    
        tables[relationshipTableName] = {
          name: relationshipTableName,
          columns,
          comment: null,
          indexes: {
            [indicesName]: {
              name: indicesName,
              unique: true,
              columns: indicesColumn,
            },
          },
        }
      }
    }
    
    for (const key in relationships) {
      if (relationships[key]?.cardinality === 'MANY_TO_MANY') {
        delete relationships[key]
      }
    }

    Copy link
    Contributor

    qodo-merge-pro-for-open-source bot commented Mar 29, 2025

    PR Code Suggestions ✨

    Explore these optional code suggestions:

    CategorySuggestion                                                                                                                                    Impact
    Possible issue
    Validate field name before usage

    Validate field.name to ensure it is a non-empty string before using it to
    construct columnName, preventing potential runtime errors.

    frontend/packages/db-structure/src/parser/prisma/parser.ts [327]

    -const columnName = `${table.model}_${field.name}`
    +const columnName = field.name && field.name.trim() !== '' 
    +  ? `${table.model}_${field.name}` 
    +  : `${table.model}_unknown`;
    • Apply this suggestion
    Suggestion importance[1-10]: 7

    __

    Why: Validating field.name ensures that it is a non-empty string before constructing columnName, which helps prevent potential runtime errors. This is a meaningful improvement in terms of robustness and error handling.

    Medium
    Validate relationship name before usage

    Validate relationship to ensure it is a valid string before using it to
    construct relationshipTableName, avoiding malformed table names.

    frontend/packages/db-structure/src/parser/prisma/parser.ts [191]

    -const relationshipTableName = `_${relationship}`
    +const relationshipTableName = relationship && relationship.trim() !== '' 
    +  ? `_${relationship}` 
    +  : '_unknown_relationship';
    • Apply this suggestion
    Suggestion importance[1-10]: 7

    __

    Why: Validating relationship ensures it is a valid string before constructing relationshipTableName, reducing the risk of malformed table names. This is a practical enhancement for error prevention and code reliability.

    Medium
    Safeguard deletion of MANY_TO_MANY relationships
    Suggestion Impact:The commit completely refactored how MANY_TO_MANY relationships are handled. Instead of deleting them at the end, the code now maintains a separate manyToManyRelationships object and manages relationships differently throughout the code. This approach addresses the underlying concern of the suggestion by eliminating the need for the deletion check altogether.

    code diff:

    +  const manyToManyRelationships: Record<string, Relationship> = {}
       const errors: Error[] = []
     
       const tableFieldRenaming: Record<string, Record<string, string>> = {}
    @@ -102,11 +103,9 @@
           if (!field.relationName) continue
     
           const existingRelationship = relationships[field.relationName]
    -      const isTargetField =
    -        field.relationToFields?.[0] &&
    -        (field.relationToFields?.length ?? 0) > 0 &&
    -        field.relationFromFields?.[0] &&
    -        (field.relationFromFields?.length ?? 0) > 0
    +      const existInManyToMany = Object.keys(manyToManyRelationships).includes(
    +        field.relationName,
    +      )
     
           const relationship: Relationship = {
             name: field.relationName,
    @@ -134,18 +133,29 @@
             ) {
               // If an existing relation is ONE_TO_ONE and this field is a list → Change it to ONE_TO_MANY
               relationship.cardinality = 'ONE_TO_MANY'
    +          relationships[relationship.name] = getFieldRenamedRelationship(
    +            relationship,
    +            tableFieldRenaming,
    +          )
             } else {
               // Otherwise, it's a MANY_TO_MANY relationship
    -          relationship.cardinality = 'MANY_TO_MANY'
    +          manyToManyRelationships[relationship.name] =
    +            getFieldRenamedRelationship(relationship, tableFieldRenaming)
             }
           } else {
    -        if (
    -          existingRelationship &&
    -          existingRelationship.cardinality === 'MANY_TO_MANY' &&
    -          isTargetField
    -        ) {
    +        if (existInManyToMany) {
               relationship.cardinality = 'ONE_TO_MANY'
    +          relationships[relationship.name] = getFieldRenamedRelationship(
    +            relationship,
    +            tableFieldRenaming,
    +          )
    +          delete manyToManyRelationships[relationship.name]
             }
    +
    +        relationships[relationship.name] = getFieldRenamedRelationship(
    +          relationship,
    +          tableFieldRenaming,
    +        )
           }
     
           relationships[relationship.name] = getFieldRenamedRelationship(
    @@ -165,12 +175,6 @@
         if (!indexInfo) continue
         table.indexes[indexInfo.name] = indexInfo
       }
    -
    -  const manyToManyRelationships = Object.fromEntries(
    -    Object.entries(relationships).filter(
    -      ([_, rel]) => rel.cardinality === 'MANY_TO_MANY',
    -    ),
    -  )
     
       if (Object.keys(manyToManyRelationships).length) {
         for (const relationship in manyToManyRelationships) {
    @@ -223,12 +227,6 @@
               },
             },
           }
    -    }
    -  }
    -
    -  for (const key in relationships) {
    -    if (relationships[key]?.cardinality === 'MANY_TO_MANY') {
    -      delete relationships[key]
         }

    Ensure that deleting relationships with cardinality 'MANY_TO_MANY' does not
    unintentionally remove related data or cause inconsistencies in the
    relationships object.

    frontend/packages/db-structure/src/parser/prisma/parser.ts [230-232]

     if (relationships[key]?.cardinality === 'MANY_TO_MANY') {
    -  delete relationships[key]
    +  // Ensure no unintended side effects before deletion
    +  if (Object.keys(relationships[key]).length > 0) {
    +    delete relationships[key];
    +  }
     }

    [Suggestion has been applied]

    Suggestion importance[1-10]: 3

    __

    Why: The suggestion adds a safeguard to ensure that the relationships[key] object is not empty before deletion. While this may prevent unintended side effects, the original code does not indicate any specific issues with deleting empty objects, making the improvement marginal.

    Low
    • Update

    @prakha
    Copy link
    Contributor Author

    prakha commented Mar 29, 2025

    Screenshot 2025-03-30 at 5 01 28 AM

    Copy link
    Member

    @MH4GF MH4GF left a comment

    Choose a reason for hiding this comment

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

    Thanks 🙏🏻

    const cardinalitySchema = v.picklist([
    'ONE_TO_ONE',
    'ONE_TO_MANY',
    'MANY_TO_MANY',
    Copy link
    Member

    Choose a reason for hiding this comment

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

    @prakha Can't you implement MANY_TO_MANY in cardinality without adding it?
    I know that during the parsing logic, I am tentatively adding and then deleting the key at the end.
    However, if it is defined here, others may misinterpret it as requiring the implementation of MANY_TO_MANY.

    Copy link
    Contributor Author

    Choose a reason for hiding this comment

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

    ok let me see what i can do.

    Copy link
    Contributor Author

    Choose a reason for hiding this comment

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

    @MH4GF ,Could you please suggest how to proceed?

    Copy link
    Member

    Choose a reason for hiding this comment

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

    @prakha
    Sorry for the wait! I wrote a sample implementation. This is a PoC that cannot be imported as is, but please use it as a reference.
    https://github.com/liam-hq/liam/pull/1179/files

    }
    }
    }
    }
    Copy link
    Member

    Choose a reason for hiding this comment

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

    And, Could you add test cases?

    prakha added 2 commits April 7, 2025 07:32
    …dinality
    
    - Update schema to only allow ONE_TO_ONE and ONE_TO_MANY
    - Move many-to-many relations to manyToManyRelationships object
    - Add better validation for relation fields
    - Preserve existing relationship constraints
    @prakha prakha closed this Apr 9, 2025
    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.

    2 participants