Repository: liam-hq/liam
Pull request #1587: ✨ Enhance the PostgreSQL parser to support Constraints
Author: login: tnyo43
Labels: name: Review effort 3/5
Created: 2025-05-05T03:11:46Z | Merged: 2025-05-12T03:40:58Z

## Issue

- resolve:

## Why is this change needed?
<!-- Please explain briefly why this change is necessary -->

We introduced constraints of schema and update tbls and Prisma parser to support the feature.
This PR will update PostgreSQL parser to support constraints.


### ref
- introduce constraints and update tbls parser https://github.com/liam-hq/liam/pull/1168
- enhance Prisma parser https://github.com/liam-hq/liam/pull/1386
- enhance schema.rb parser https://github.com/liam-hq/liam/pull/1408

## What would you like reviewers to focus on?
<!-- What specific aspects are you requesting review for? -->

- the constraints are parsed properly
  - The PostgreSQL parser will support "PRIMARY KEY", "FOREIGN KEY", "UNIQUE" and "CHECK" constraints.
- the tests are appropriate

## Testing Verification
<!-- Please describe how you verified these changes in your local environment using text/images/video -->

- 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](https://github.com/liam-hq/liam/blob/4271e7b9af6f73e142678f73b3a7cd293f0a641b/frontend/packages/db-structure/src/parser/sql/input/postgresql_schema1.in.sql))
  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 --format postgres --input schema.in.sql` to generate scripts
  4. run `npx http-server dist` to run the application and go to http://localhost:8080/
  5. click "users" and "posts" tables and see the Constraints section of its table details

<details>
<summary>schema.in.sql</summary>

```
CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  username VARCHAR(255) NOT NULL UNIQUE,
  email VARCHAR(255) NOT NULL UNIQUE CHECK (char_length(name) <= 255),
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE posts (
  id SERIAL PRIMARY KEY,
  user_id INT REFERENCES users(id)
);

CREATE UNIQUE INDEX index_users_on_id_and_email ON public.users USING btree (id, email);
COMMENT ON TABLE users IS 'store our users.';
COMMENT ON COLUMN users.username IS 'user fullname.';
```

</details>

|users|posts|
|-|-|
|![Screenshot 0007-05-05 at 12 31 27](https://github.com/user-attachments/assets/b28bb233-9ebd-4115-9c16-8686cd5e9d33)|![Screenshot 0007-05-05 at 12 32 06](https://github.com/user-attachments/assets/b7110f8f-443f-4022-8232-8ddb36049176)|




## What was done
<!-- This section will be filled by PR-Agent when the Pull Request is opened -->

### 🤖 Generated by PR Agent at 342d6e009505a29ce3b09e141ad5863338213297

- Add support for parsing PostgreSQL table constraints:
    - PRIMARY KEY, UNIQUE, FOREIGN KEY, and CHECK constraints now parsed.
    - Constraints are extracted from both column and table definitions.
- Update schema conversion logic to include constraints in table objects.
- Extend and enhance test coverage for constraint parsing.
- Update processor interface to pass raw SQL for CHECK constraint extraction.


## Detailed Changes
<!-- This section will be filled by PR-Agent when the Pull Request is opened -->

<table><thead><tr><th></th><th align="left">Relevant files</th></tr></thead><tbody><tr><td><strong>Enhancement</strong></td><td><table>
<tr>
  <td>
    <details>
      <summary><strong>converter.ts</strong><dd><code>Add constraint extraction and storage to PostgreSQL schema converter</code></dd></summary>
<hr>

frontend/packages/db-structure/src/parser/sql/postgresql/converter.ts

<li>Add logic to extract PRIMARY KEY, UNIQUE, FOREIGN KEY, and CHECK <br>constraints.<br> <li> Refactor relationship extraction to also return foreign key <br>constraints.<br> <li> Implement parsing of CHECK constraint expressions from raw SQL.<br> <li> Store constraints in the table schema output.<br> <li> Update index and ALTER TABLE handling to preserve constraints.


</details>


  </td>
  <td><a href="https://github.com/liam-hq/liam/pull/1587/files#diff-bae74cb1e5472bf6f333ebc48a79fe05fa5d0b74268d5cd067967896ef0f2cb8">+132/-44</a></td>

</tr>

<tr>
  <td>
    <details>
      <summary><strong>index.ts</strong><dd><code>Update processor to support raw SQL for constraint parsing</code></dd></summary>
<hr>

frontend/packages/db-structure/src/parser/sql/postgresql/index.ts

<li>Pass raw SQL to schema converter for CHECK constraint extraction.<br> <li> Update processor to accommodate new converter signature.


</details>


  </td>
  <td><a href="https://github.com/liam-hq/liam/pull/1587/files#diff-e947976bc36e479c39dfed57edcc5c82e7e189f08fe1bf6b4e7f4bffbc19aa04">+1/-0</a>&nbsp; &nbsp; &nbsp; </td>

</tr>
</table></td></tr><tr><td><strong>Tests</strong></td><td><table>
<tr>
  <td>
    <details>
      <summary><strong>index.test.ts</strong><dd><code>Add and update tests for constraint parsing</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

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

<li>Add and update tests for PRIMARY KEY, UNIQUE, FOREIGN KEY, and CHECK <br>constraints.<br> <li> Assert that constraints are correctly parsed and included in output.<br> <li> Enhance test coverage for new constraint logic.


</details>


  </td>
  <td><a href="https://github.com/liam-hq/liam/pull/1587/files#diff-2ffdcf3d84e15e4ef7dd163c02f09c80bd750fb31b3effe125fc475c73bdf936">+81/-1</a>&nbsp; &nbsp; </td>

</tr>
</table></td></tr><tr><td><strong>Documentation</strong></td><td><table>
<tr>
  <td>
    <details>
      <summary><strong>olive-women-notice.md</strong><dd><code>Add changeset for PostgreSQL constraint support</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

.changeset/olive-women-notice.md

<li>Add changeset describing enhancement of PostgreSQL parser to support <br>constraints.


</details>


  </td>
  <td><a href="https://github.com/liam-hq/liam/pull/1587/files#diff-1951d54764740ab79f74d355c5714d1a6d3b10518bb5bc56e9ed7d6cf1ec44c4">+5/-0</a>&nbsp; &nbsp; &nbsp; </td>

</tr>
</table></td></tr></tr></tbody></table>

## Additional Notes
<!-- Any additional information for reviewers -->

___

> <details> <summary>  Need help?</summary><li>Type <code>/help how to ...</code> in the comments thread for any questions about Qodo Merge usage.</li><li>Check out the <a href="https://qodo-merge-docs.qodo.ai/usage-guide/">documentation</a> for more information.</li></details>


Diff:
diff --git a/.changeset/olive-women-notice.md b/.changeset/olive-women-notice.md
new file mode 100644
index 0000000000..feb4c2df3b
--- /dev/null
+++ b/.changeset/olive-women-notice.md
@@ -0,0 +1,5 @@
+---
+"@liam-hq/db-structure": patch
+---
+
+✨ Enhance the postgresql parser to support Constraints
diff --git a/frontend/packages/db-structure/src/parser/__snapshots__/index.test.ts.snap b/frontend/packages/db-structure/src/parser/__snapshots__/index.test.ts.snap
index 6658765a68..916a2dfe8a 100644
--- a/frontend/packages/db-structure/src/parser/__snapshots__/index.test.ts.snap
+++ b/frontend/packages/db-structure/src/parser/__snapshots__/index.test.ts.snap
@@ -40,7 +40,22 @@ exports[`parse > should parse postgresql to JSON correctly 1`] = `
         },
       },
       "comment": null,
-      "constraints": {},
+      "constraints": {
+        "PRIMARY_id": {
+          "columnName": "id",
+          "name": "PRIMARY_id",
+          "type": "PRIMARY KEY",
+        },
+        "users_id_to_posts_user_id": {
+          "columnName": "user_id",
+          "deleteConstraint": "NO_ACTION",
+          "name": "users_id_to_posts_user_id",
+          "targetColumnName": "id",
+          "targetTableName": "users",
+          "type": "FOREIGN KEY",
+          "updateConstraint": "NO_ACTION",
+        },
+      },
       "indexes": {},
       "name": "posts",
     },
@@ -88,7 +103,23 @@ exports[`parse > should parse postgresql to JSON correctly 1`] = `
         },
       },
       "comment": "store our users.",
-      "constraints": {},
+      "constraints": {
+        "PRIMARY_id": {
+          "columnName": "id",
+          "name": "PRIMARY_id",
+          "type": "PRIMARY KEY",
+        },
+        "UNIQUE_email": {
+          "columnName": "email",
+          "name": "UNIQUE_email",
+          "type": "UNIQUE",
+        },
+        "UNIQUE_username": {
+          "columnName": "username",
+          "name": "UNIQUE_username",
+          "type": "UNIQUE",
+        },
+      },
       "indexes": {
         "index_users_on_id_and_email": {
           "columns": [
diff --git a/frontend/packages/db-structure/src/parser/sql/postgresql/converter.ts b/frontend/packages/db-structure/src/parser/sql/postgresql/converter.ts
index 60ee4f4c0d..b2d0e5ec0c 100644
--- a/frontend/packages/db-structure/src/parser/sql/postgresql/converter.ts
+++ b/frontend/packages/db-structure/src/parser/sql/postgresql/converter.ts
@@ -1,17 +1,21 @@
 import type {
   AlterTableStmt,
   CommentStmt,
-  Constraint,
   CreateStmt,
   IndexStmt,
   List,
   Node,
+  Constraint as PgConstraint,
   String as PgString,
   RawStmt,
 } from '@pgsql/types'
 import { type Result, err, ok } from 'neverthrow'
 import type {
+  CheckConstraint,
   Columns,
+  Constraint,
+  Constraints,
+  ForeignKeyConstraint,
   ForeignKeyConstraintReferenceOption,
   Relationship,
   Table,
@@ -35,8 +39,8 @@ function isStringNode(node: Node | undefined): node is { String: PgString } {
   )
 }
 
-function isConstraintNode(node: Node): node is { Constraint: Constraint } {
-  return (node as { Constraint: Constraint }).Constraint !== undefined
+function isConstraintNode(node: Node): node is { Constraint: PgConstraint } {
+  return (node as { Constraint: PgConstraint }).Constraint !== undefined
 }
 
 // ON UPDATE or ON DELETE subclauses for foreign key
@@ -70,7 +74,7 @@ function extractDefaultValueFromConstraints(
 
   const constraintNodes = constraints.filter(isConstraintNode)
   for (const c of constraintNodes) {
-    const constraint = (c as { Constraint: Constraint }).Constraint
+    const constraint = (c as { Constraint: PgConstraint }).Constraint
 
     // Skip if not a default constraint or missing required properties
     if (
@@ -102,13 +106,18 @@ function extractDefaultValueFromConstraints(
   return null
 }
 
-const constraintToRelationship = (
+const constraintToRelationshipAndForeignKeyConstraint = (
   foreignTableName: string,
   foreignColumnName: string,
-  constraint: Constraint,
-): Result<Relationship | undefined, UnexpectedTokenWarningError> => {
+  constraint: PgConstraint,
+): Result<
+  [Relationship, ForeignKeyConstraint],
+  UnexpectedTokenWarningError
+> => {
   if (constraint.contype !== 'CONSTR_FOREIGN') {
-    return ok(undefined)
+    return err(
+      new UnexpectedTokenWarningError('contype "CONSTR_FOREIGN" is expected'),
+    )
   }
 
   const primaryTableName = constraint.pktable?.relname
@@ -134,7 +143,7 @@ const constraintToRelationship = (
   const deleteConstraint = getConstraintAction(constraint.fk_del_action)
   const cardinality = 'ONE_TO_MANY'
 
-  return ok({
+  const relationship: Relationship = {
     name,
     primaryTableName,
     primaryColumnName,
@@ -143,11 +152,68 @@ const constraintToRelationship = (
     cardinality,
     updateConstraint,
     deleteConstraint,
-  })
+  }
+  const foreignKeyConstraint: ForeignKeyConstraint = {
+    type: 'FOREIGN KEY',
+    name,
+    columnName: foreignColumnName,
+    targetTableName: primaryTableName,
+    targetColumnName: primaryColumnName,
+    updateConstraint,
+    deleteConstraint,
+  }
+
+  return ok([relationship, foreignKeyConstraint])
+}
+
+const constraintToCheckConstraint = (
+  columnName: string | undefined,
+  constraint: PgConstraint,
+  rawSql: string,
+): Result<CheckConstraint, UnexpectedTokenWarningError> => {
+  if (constraint.contype !== 'CONSTR_CHECK') {
+    return err(
+      new UnexpectedTokenWarningError('contype "CONSTR_CHECK" is expected'),
+    )
+  }
+
+  if (constraint.location === undefined) {
+    return err(new UnexpectedTokenWarningError('Invalid check constraint'))
+  }
+
+  let openParenthesesCount = 0
+  const startLocation = rawSql.indexOf('(', constraint.location)
+  let endLocation: number | undefined = undefined
+  for (let i = startLocation; i < rawSql.length; i++) {
+    if (rawSql[i] === '(') {
+      openParenthesesCount++
+    } else if (rawSql[i] === ')') {
+      openParenthesesCount--
+      if (openParenthesesCount === 0) {
+        endLocation = i
+        break
+      }
+    }
+  }
+
+  if (startLocation === undefined || endLocation === undefined) {
+    return err(new UnexpectedTokenWarningError('Invalid check constraint'))
+  }
+
+  const checkConstraint: CheckConstraint = {
+    name: constraint.conname ?? `CHECK_${columnName}`,
+    type: 'CHECK',
+    detail: `CHECK ${rawSql.slice(startLocation, endLocation + 1)}`,
+  }
+
+  return ok(checkConstraint)
 }
 
 // Transform function for AST to Schema
-export const convertToSchema = (stmts: RawStmt[]): ProcessResult => {
+export const convertToSchema = (
+  stmts: RawStmt[],
+  rawSql: string,
+): ProcessResult => {
   const tables: Record<string, Table> = {}
   const relationships: Record<string, Relationship> = {}
   const tableGroups: Record<string, TableGroup> = {}
@@ -208,7 +274,6 @@ export const convertToSchema = (stmts: RawStmt[]): ProcessResult => {
         ) || false
     )
   }
-
   /**
    * Check if a column is not null
    */
@@ -246,6 +311,55 @@ export const convertToSchema = (stmts: RawStmt[]): ProcessResult => {
     comment: string | null
   }
 
+  // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: <explanation>
+  function processRelationshipsAndConstraints(
+    tableName: string,
+    columnName: string,
+    _constraints: Node[],
+  ): {
+    relationships: Relationship[]
+    constraints: Constraint[]
+    columnErrors: ProcessError[]
+  } {
+    const relationships: Relationship[] = []
+    const constraints: Constraint[] = []
+    const columnErrors: ProcessError[] = []
+
+    for (const constraint of _constraints.filter(isConstraintNode)) {
+      if (constraint.Constraint.contype === 'CONSTR_FOREIGN') {
+        const relResult = constraintToRelationshipAndForeignKeyConstraint(
+          tableName,
+          columnName,
+          constraint.Constraint,
+        )
+
+        if (relResult.isErr()) {
+          columnErrors.push(relResult.error)
+          continue
+        }
+
+        const [relationship, foreignKeyConstraint] = relResult.value
+        relationships.push(relationship)
+        constraints.push(foreignKeyConstraint)
+      } else if (constraint.Constraint.contype === 'CONSTR_CHECK') {
+        const relResult = constraintToCheckConstraint(
+          columnName,
+          constraint.Constraint,
+          rawSql,
+        )
+
+        if (relResult.isErr()) {
+          errors.push(relResult.error)
+          continue
+        }
+        const checkConstraint = relResult.value
+        constraints.push(checkConstraint)
+      }
+    }
+
+    return { relationships, constraints, columnErrors }
+  }
+
   /**
    * Process a column definition
    */
@@ -254,6 +368,7 @@ export const convertToSchema = (stmts: RawStmt[]): ProcessResult => {
     tableName: string,
   ): {
     column: [string, Column]
+    constraints: Constraint[]
     relationships: Relationship[]
     errors: ProcessError[]
   } {
@@ -274,13 +389,18 @@ export const convertToSchema = (stmts: RawStmt[]): ProcessResult => {
             comment: null,
           },
         ],
+        constraints: [],
         relationships: [],
         errors: [],
       }
     }
 
-    const columnErrors: ProcessError[] = []
-    const columnRelationships: Relationship[] = []
+    const { constraints, relationships, columnErrors } =
+      processRelationshipsAndConstraints(
+        tableName,
+        columnName,
+        colDef.constraints ?? [],
+      )
 
     // Create column object
     const column = {
@@ -294,29 +414,26 @@ export const convertToSchema = (stmts: RawStmt[]): ProcessResult => {
       comment: null, // TODO
     }
 
-    // Process relationships from constraints
-    for (const constraint of (colDef.constraints ?? []).filter(
-      isConstraintNode,
-    )) {
-      const relResult = constraintToRelationship(
-        tableName,
+    if (isPrimaryKey(colDef.constraints)) {
+      const constraintName = `PRIMARY_${columnName}`
+      constraints.push({
+        name: constraintName,
+        type: 'PRIMARY KEY',
         columnName,
-        constraint.Constraint,
-      )
-
-      if (relResult.isErr()) {
-        columnErrors.push(relResult.error)
-        continue
-      }
-
-      if (relResult.value !== undefined) {
-        columnRelationships.push(relResult.value)
-      }
+      })
+    } else if (isUnique(colDef.constraints)) {
+      const constraintName = `UNIQUE_${columnName}`
+      constraints.push({
+        name: constraintName,
+        type: 'UNIQUE',
+        columnName,
+      })
     }
 
     return {
       column: [columnName, column],
-      relationships: columnRelationships,
+      constraints,
+      relationships,
       errors: columnErrors,
     }
   }
@@ -329,10 +446,12 @@ export const convertToSchema = (stmts: RawStmt[]): ProcessResult => {
     tableName: string,
   ): {
     columns: Columns
+    constraints: Constraints
     tableRelationships: Relationship[]
     tableErrors: ProcessError[]
   } {
     const columns: Columns = {}
+    const constraints: Constraints = {}
     const tableRelationships: Relationship[] = []
     const tableErrors: ProcessError[] = []
 
@@ -341,6 +460,7 @@ export const convertToSchema = (stmts: RawStmt[]): ProcessResult => {
       if ('ColumnDef' in elt) {
         const {
           column,
+          constraints: columnConstraints,
           relationships: colRelationships,
           errors: colErrors,
         } = processColumnDef(elt.ColumnDef, tableName)
@@ -349,24 +469,31 @@ export const convertToSchema = (stmts: RawStmt[]): ProcessResult => {
           columns[column[0]] = column[1]
         }
 
+        for (const constraint of columnConstraints) {
+          constraints[constraint.name] = constraint
+        }
         tableRelationships.push(...colRelationships)
         tableErrors.push(...colErrors)
       }
     }
 
-    return { columns, tableRelationships, tableErrors }
+    return { columns, constraints, tableRelationships, tableErrors }
   }
 
   /**
    * Create a table object
    */
-  function createTableObject(tableName: string, columns: Columns): void {
+  function createTableObject(
+    tableName: string,
+    columns: Columns,
+    constraints: Constraints,
+  ): void {
     tables[tableName] = {
       name: tableName,
       columns,
       comment: null,
       indexes: {},
-      constraints: {},
+      constraints,
     }
   }
 
@@ -381,13 +508,11 @@ export const convertToSchema = (stmts: RawStmt[]): ProcessResult => {
     if (!tableName) return
 
     // Process table elements
-    const { columns, tableRelationships, tableErrors } = processTableElements(
-      createStmt.tableElts,
-      tableName,
-    )
+    const { columns, constraints, tableRelationships, tableErrors } =
+      processTableElements(createStmt.tableElts, tableName)
 
     // Create table object
-    createTableObject(tableName, columns)
+    createTableObject(tableName, columns, constraints)
 
     // Add relationships and errors
     for (const relationship of tableRelationships) {
@@ -443,7 +568,7 @@ export const convertToSchema = (stmts: RawStmt[]): ProcessResult => {
             type,
           },
         },
-        constraints: {},
+        constraints: tables[tableName]?.constraints || {},
       }
     }
   }
@@ -564,20 +689,19 @@ export const convertToSchema = (stmts: RawStmt[]): ProcessResult => {
    */
   function processForeignKeyConstraint(
     foreignTableName: string,
-    constraint: { Constraint: Constraint },
+    constraint: PgConstraint,
   ): void {
     const foreignColumnName =
-      constraint.Constraint.fk_attrs?.[0] &&
-      isStringNode(constraint.Constraint.fk_attrs[0])
-        ? constraint.Constraint.fk_attrs[0].String.sval
+      constraint.fk_attrs?.[0] && isStringNode(constraint.fk_attrs[0])
+        ? constraint.fk_attrs[0].String.sval
         : undefined
 
     if (foreignColumnName === undefined) return
 
-    const relResult = constraintToRelationship(
+    const relResult = constraintToRelationshipAndForeignKeyConstraint(
       foreignTableName,
       foreignColumnName,
-      constraint.Constraint,
+      constraint,
     )
 
     if (relResult.isErr()) {
@@ -585,10 +709,33 @@ export const convertToSchema = (stmts: RawStmt[]): ProcessResult => {
       return
     }
 
-    if (relResult.value === undefined) return
+    const [relationship, foreignKeyConstraint] = relResult.value
 
-    const relationship = relResult.value
     relationships[relationship.name] = relationship
+    const table = tables[foreignTableName]
+    if (table) {
+      table.constraints[foreignKeyConstraint.name] = foreignKeyConstraint
+    }
+  }
+
+  /**
+   * Process a check constraint
+   */
+  function processCheckConstraint(
+    foreignTableName: string,
+    constraint: PgConstraint,
+  ): void {
+    const relResult = constraintToCheckConstraint(undefined, constraint, rawSql)
+
+    if (relResult.isErr()) {
+      errors.push(relResult.error)
+      return
+    }
+
+    const table = tables[foreignTableName]
+    if (table) {
+      table.constraints[relResult.value.name] = relResult.value
+    }
   }
 
   /**
@@ -610,13 +757,18 @@ export const convertToSchema = (stmts: RawStmt[]): ProcessResult => {
 
     const alterTableCmd = cmd.AlterTableCmd
 
-    // Only process ADD CONSTRAINT commands
-    if (alterTableCmd.subtype !== 'AT_AddConstraint') return
+    if (alterTableCmd.subtype !== 'AT_AddConstraint')
+      // Only process ADD CONSTRAINT commands
+      return
 
     const constraint = alterTableCmd.def
     if (!constraint || !isConstraintNode(constraint)) return
 
-    processForeignKeyConstraint(foreignTableName, constraint)
+    if (constraint.Constraint.contype === 'CONSTR_FOREIGN') {
+      processForeignKeyConstraint(foreignTableName, constraint.Constraint)
+    } else if (constraint.Constraint.contype === 'CONSTR_CHECK') {
+      processCheckConstraint(foreignTableName, constraint.Constraint)
+    }
   }
 
   /**
diff --git a/frontend/packages/db-structure/src/parser/sql/postgresql/index.test.ts b/frontend/packages/db-structure/src/parser/sql/postgresql/index.test.ts
index 3628864dcf..c7c48db88f 100644
--- a/frontend/packages/db-structure/src/parser/sql/postgresql/index.test.ts
+++ b/frontend/packages/db-structure/src/parser/sql/postgresql/index.test.ts
@@ -25,6 +25,14 @@ describe(processor, () => {
             ...override?.indexes,
           },
           comment: override?.comment ?? null,
+          constraints: {
+            ...override?.constraints,
+            PRIMARY_id: {
+              name: 'PRIMARY_id',
+              type: 'PRIMARY KEY',
+              columnName: 'id',
+            },
+          },
         }),
       },
     })
@@ -117,7 +125,24 @@ describe(processor, () => {
         );
       `)
 
-      expect(value).toEqual(parserTestCases.unique)
+      expect(value).toEqual(
+        userTable({
+          columns: {
+            mention: aColumn({
+              name: 'mention',
+              type: 'text',
+              unique: true,
+            }),
+          },
+          constraints: {
+            UNIQUE_mention: {
+              name: 'UNIQUE_mention',
+              type: 'UNIQUE',
+              columnName: 'mention',
+            },
+          },
+        }),
+      )
     })
 
     it('index (unique: false)', async () => {
@@ -178,6 +203,22 @@ describe(processor, () => {
           'users_id_to_posts_user_id',
         ),
       )
+      expect(value.tables['posts']?.constraints).toEqual({
+        PRIMARY_id: {
+          name: 'PRIMARY_id',
+          type: 'PRIMARY KEY',
+          columnName: 'id',
+        },
+        users_id_to_posts_user_id: {
+          name: 'users_id_to_posts_user_id',
+          type: 'FOREIGN KEY',
+          columnName: 'user_id',
+          targetColumnName: 'id',
+          targetTableName: 'users',
+          updateConstraint: 'NO_ACTION',
+          deleteConstraint: 'NO_ACTION',
+        },
+      })
     })
 
     it('foreign key (one-to-one)', async () => {
@@ -192,6 +233,50 @@ describe(processor, () => {
       expect(value.relationships).toEqual(
         parserTestCases['foreign key (one-to-one)'](keyName),
       )
+      expect(value.tables['posts']?.constraints).toEqual({
+        PRIMARY_id: {
+          name: 'PRIMARY_id',
+          type: 'PRIMARY KEY',
+          columnName: 'id',
+        },
+        UNIQUE_user_id: {
+          name: 'UNIQUE_user_id',
+          type: 'UNIQUE',
+          columnName: 'user_id',
+        },
+        users_id_to_posts_user_id: {
+          name: 'users_id_to_posts_user_id',
+          type: 'FOREIGN KEY',
+          columnName: 'user_id',
+          targetColumnName: 'id',
+          targetTableName: 'users',
+          updateConstraint: 'NO_ACTION',
+          deleteConstraint: 'NO_ACTION',
+        },
+      })
+    })
+
+    it('check constraint', async () => {
+      const { value } = await processor(/* sql */ `
+        CREATE TABLE products (
+            id SERIAL PRIMARY KEY,
+            name text,
+            price numeric CHECK (price > 0)
+        );
+      `)
+
+      expect(value.tables['products']?.constraints).toEqual({
+        PRIMARY_id: {
+          name: 'PRIMARY_id',
+          type: 'PRIMARY KEY',
+          columnName: 'id',
+        },
+        CHECK_price: {
+          name: 'CHECK_price',
+          type: 'CHECK',
+          detail: 'CHECK (price > 0)',
+        },
+      })
     })
   })
 
@@ -199,6 +284,11 @@ describe(processor, () => {
     it('foreign key (one-to-many)', async () => {
       const keyName = 'fk_posts_user_id'
       const { value } = await processor(/* sql */ `
+        CREATE TABLE posts (
+            id SERIAL PRIMARY KEY,
+            user_id INT NOT NULL
+        );
+
         ALTER TABLE posts
         ADD CONSTRAINT ${keyName} FOREIGN KEY (user_id) REFERENCES users(id);
       `)
@@ -206,6 +296,22 @@ describe(processor, () => {
       expect(value.relationships).toEqual(
         parserTestCases['foreign key (one-to-many)'](keyName),
       )
+      expect(value.tables['posts']?.constraints).toEqual({
+        PRIMARY_id: {
+          name: 'PRIMARY_id',
+          type: 'PRIMARY KEY',
+          columnName: 'id',
+        },
+        fk_posts_user_id: {
+          name: 'fk_posts_user_id',
+          type: 'FOREIGN KEY',
+          columnName: 'user_id',
+          targetTableName: 'users',
+          targetColumnName: 'id',
+          updateConstraint: 'NO_ACTION',
+          deleteConstraint: 'NO_ACTION',
+        },
+      })
     })
 
     it('foreign key (one-to-one)', async () => {
@@ -223,6 +329,27 @@ describe(processor, () => {
       expect(value.relationships).toEqual(
         parserTestCases['foreign key (one-to-one)'](keyName),
       )
+      expect(value.tables['posts']?.constraints).toEqual({
+        PRIMARY_id: {
+          name: 'PRIMARY_id',
+          type: 'PRIMARY KEY',
+          columnName: 'id',
+        },
+        UNIQUE_user_id: {
+          name: 'UNIQUE_user_id',
+          type: 'UNIQUE',
+          columnName: 'user_id',
+        },
+        users_id_to_posts_user_id: {
+          name: 'users_id_to_posts_user_id',
+          type: 'FOREIGN KEY',
+          columnName: 'user_id',
+          targetTableName: 'users',
+          targetColumnName: 'id',
+          updateConstraint: 'NO_ACTION',
+          deleteConstraint: 'NO_ACTION',
+        },
+      })
     })
 
     it('foreign key with action', async () => {
@@ -239,6 +366,48 @@ describe(processor, () => {
       expect(value.relationships).toEqual(
         parserTestCases['foreign key with action'],
       )
+      expect(value.tables['posts']?.constraints).toEqual({
+        PRIMARY_id: {
+          name: 'PRIMARY_id',
+          type: 'PRIMARY KEY',
+          columnName: 'id',
+        },
+        fk_posts_user_id: {
+          name: 'fk_posts_user_id',
+          type: 'FOREIGN KEY',
+          columnName: 'user_id',
+          targetTableName: 'users',
+          targetColumnName: 'id',
+          updateConstraint: 'RESTRICT',
+          deleteConstraint: 'CASCADE',
+        },
+      })
+    })
+
+    it('check constraint', async () => {
+      const { value } = await processor(/* sql */ `
+        CREATE TABLE products (
+            id SERIAL PRIMARY KEY,
+            name text,
+            price numeric
+        );
+
+        ALTER TABLE products
+        ADD CONSTRAINT price_check_is_positive CHECK (price > 0);
+      `)
+
+      expect(value.tables['products']?.constraints).toEqual({
+        PRIMARY_id: {
+          name: 'PRIMARY_id',
+          type: 'PRIMARY KEY',
+          columnName: 'id',
+        },
+        price_check_is_positive: {
+          name: 'price_check_is_positive',
+          type: 'CHECK',
+          detail: 'CHECK (price > 0)',
+        },
+      })
     })
   })
 
diff --git a/frontend/packages/db-structure/src/parser/sql/postgresql/index.ts b/frontend/packages/db-structure/src/parser/sql/postgresql/index.ts
index b393858395..f417a34777 100644
--- a/frontend/packages/db-structure/src/parser/sql/postgresql/index.ts
+++ b/frontend/packages/db-structure/src/parser/sql/postgresql/index.ts
@@ -54,6 +54,7 @@ async function processChunk(
   chunk: string,
   schema: Schema,
   parseErrors: ProcessError[],
+  rawSql: string,
 ): Promise<[number | null, number | null, ProcessError[]]> {
   let readOffset: number | null = null
   let retryOffset: number | null = null
@@ -83,6 +84,7 @@ async function processChunk(
 
   const { value: convertedSchema, errors: conversionErrors } = convertToSchema(
     isLastStatementComplete ? parse_tree.stmts : parse_tree.stmts.slice(0, -1),
+    rawSql,
   )
 
   if (conversionErrors !== null) {
@@ -113,7 +115,7 @@ export const processor: Processor = async (sql: string) => {
   const parseErrors: ProcessError[] = []
 
   const errors = await processSQLInChunks(sql, CHUNK_SIZE, async (chunk) => {
-    return processChunk(chunk, schema, parseErrors)
+    return processChunk(chunk, schema, parseErrors, sql)
   })
 
   return { value: schema, errors: parseErrors.concat(errors) }


Reviews:
user: login: tnyo43
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T03:44:22Z
user: login: tnyo43
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T04:12:39Z
user: login: MH4GF
body: This was more difficult problem than I thought. Thanks for implementing!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T10:16:36Z
user: login: tnyo43
body: I modified it a little since https://github.com/liam-hq/liam/pull/1583 is merged.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-10T07:55:16Z
user: login: MH4GF
body: Thanks!! Nice work 👍🏻 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T00:54:38Z
user: login: FunamaYukina
body: LGTM! Thank you so much!!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-12T03:34:58Z
user: login: pyramation
state: COMMENTED
author_association: NONE
submitted_at: 2025-06-23T04:00:13Z

Review comments:
diff_hunk: @@ -136,11 +198,64 @@ export const convertToSchema = (stmts: RawStmt[]): ProcessResult => {
     if (!tableName) return
 
     const columns: Columns = {}
+    const constraints: Constraints = {}
     for (const elt of createStmt.tableElts) {
       if ('ColumnDef' in elt) {
         const colDef = elt.ColumnDef
         if (colDef.colname === undefined) continue
 
+        let isPrimary = false
+        let isUnique = false
+        for (const constraint of colDef.constraints ?? []) {
path: frontend/packages/db-structure/src/parser/sql/postgresql/converter.ts
user: login: tnyo43
body: This for loop is to unify the logics with constraints check.
It refers to `colDef.constraints` objects to create constraint objects, relationship objects and make flag if the columns are primary or unique, which currently used to create `Column` object.
created_at: 2025-05-05T03:42:50Z
author_association: MEMBER
diff_hunk: @@ -97,11 +105,65 @@ const constraintToRelationship = (
     cardinality,
     updateConstraint,
     deleteConstraint,
-  })
+  }
+  const foreignKeyConstraint: ForeignKeyConstraint = {
+    type: 'FOREIGN KEY',
+    name,
+    columnName: foreignColumnName,
+    targetTableName: primaryTableName,
+    targetColumnName: primaryColumnName,
+    updateConstraint,
+    deleteConstraint,
+  }
+
+  return ok([relationship, foreignKeyConstraint])
+}
+
+const constraintToCheckConstraint = (
+  columnName: string,
+  constraint: Constraint,
+  rawSql: string,
+): Result<CheckConstraint, UnexpectedTokenWarningError> => {
+  if (constraint.contype !== 'CONSTR_CHECK') {
+    return err(
+      new UnexpectedTokenWarningError('contype "CONSTR_CHECK" is expected'),
+    )
+  }
+
+  if (constraint.location === undefined) {
+    return err(new UnexpectedTokenWarningError('Invalid check constraint'))
+  }
+  let openParenthesesCount = 0
+  let endLocation: number | undefined = undefined
+  for (let i = constraint.location; i < rawSql.length; i++) {
+    if (rawSql[i] === '(') openParenthesesCount++
+    else if (rawSql[i] === ')') {
+      openParenthesesCount--
+      if (openParenthesesCount === 0) {
+        endLocation = i
+        break
+      }
+    }
+  }
+
+  if (endLocation === undefined) {
+    return err(new UnexpectedTokenWarningError('Invalid check constraint'))
+  }
+
+  const checkConstraint: CheckConstraint = {
+    name: `CHECK_${columnName}`,
+    type: 'CHECK',
+    detail: rawSql.slice(constraint.location, endLocation + 1),
+  }
+
+  return ok(checkConstraint)
 }
 
 // Transform function for AST to Schema
-export const convertToSchema = (stmts: RawStmt[]): ProcessResult => {
+export const convertToSchema = (
+  stmts: RawStmt[],
+  rawSql: string,
path: frontend/packages/db-structure/src/parser/sql/postgresql/converter.ts
user: login: tnyo43
body: added this argument for `constraintToCheckConstraint` function.
For the detail: https://github.com/liam-hq/liam/pull/1587#discussion_r2072821225
created_at: 2025-05-05T03:44:19Z
author_association: MEMBER
diff_hunk: @@ -56,13 +59,18 @@ function getConstraintAction(
   }
 }
 
-const constraintToRelationship = (
+const constraintToRelationshipAndForeignKeyConstraint = (
   foreignTableName: string,
   foreignColumnName: string,
   constraint: Constraint,
-): Result<Relationship | undefined, UnexpectedTokenWarningError> => {
+): Result<
+  [Relationship, ForeignKeyConstraint],
+  UnexpectedTokenWarningError
+> => {
   if (constraint.contype !== 'CONSTR_FOREIGN') {
-    return ok(undefined)
+    return err(
+      new UnexpectedTokenWarningError('contype "CONSTR_FOREIGN" is expected'),
+    )
path: frontend/packages/db-structure/src/parser/sql/postgresql/converter.ts
user: login: tnyo43
body: [Because of this change](https://github.com/liam-hq/liam/pull/1587/files#r2072813689), the objects of `constraint` variable must always satisfy `constraint.contype !== 'CONSTR_FOREIGN'`, so that I modified this to return an error state if it doesn't.
created_at: 2025-05-05T03:46:25Z
author_association: MEMBER
diff_hunk: @@ -97,11 +105,65 @@ const constraintToRelationship = (
     cardinality,
     updateConstraint,
     deleteConstraint,
-  })
+  }
+  const foreignKeyConstraint: ForeignKeyConstraint = {
+    type: 'FOREIGN KEY',
+    name,
+    columnName: foreignColumnName,
+    targetTableName: primaryTableName,
+    targetColumnName: primaryColumnName,
+    updateConstraint,
+    deleteConstraint,
+  }
+
+  return ok([relationship, foreignKeyConstraint])
+}
+
+const constraintToCheckConstraint = (
+  columnName: string,
+  constraint: Constraint,
+  rawSql: string,
+): Result<CheckConstraint, UnexpectedTokenWarningError> => {
+  if (constraint.contype !== 'CONSTR_CHECK') {
+    return err(
+      new UnexpectedTokenWarningError('contype "CONSTR_CHECK" is expected'),
+    )
+  }
+
+  if (constraint.location === undefined) {
+    return err(new UnexpectedTokenWarningError('Invalid check constraint'))
+  }
+  let openParenthesesCount = 0
+  let endLocation: number | undefined = undefined
+  for (let i = constraint.location; i < rawSql.length; i++) {
+    if (rawSql[i] === '(') openParenthesesCount++
+    else if (rawSql[i] === ')') {
+      openParenthesesCount--
+      if (openParenthesesCount === 0) {
+        endLocation = i
+        break
+      }
+    }
+  }
path: frontend/packages/db-structure/src/parser/sql/postgresql/converter.ts
user: login: tnyo43
body: This is a logic to extract `detail` value of check constraint from the original SQL text.
The `Constraint`(of @pgsql) object has `.location` field which indicates the position where the constraint object is parsed from the original SQL text.

This is an example:
If the original SQL string is `"CHECK (a(b)c(d(e)))fgh"` and the `constraint.location` is 6, the substring `(a(b)c(d(e)))fgh` is scanned from the beginning. The value of `openParenthesesCount` changes as 1, 1, 2, 1, 1, 2, 2, 3, 2, 1, 0 while reading the string character by character. The position where the count returns to 0 is marked as `endLocation` (this case `endLocation` is 18).  The correct expression (a(b)c(d(e))) can be extracted by executing `rawSql.slice(constraint.location, endLocation + 1)`.

> A check constraint consists of the key word CHECK followed by an expression in parentheses.
https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-CHECK-CONSTRAINTS


created_at: 2025-05-05T04:03:05Z
author_association: MEMBER
diff_hunk: @@ -97,11 +105,65 @@ const constraintToRelationship = (
     cardinality,
     updateConstraint,
     deleteConstraint,
-  })
+  }
+  const foreignKeyConstraint: ForeignKeyConstraint = {
+    type: 'FOREIGN KEY',
+    name,
+    columnName: foreignColumnName,
+    targetTableName: primaryTableName,
+    targetColumnName: primaryColumnName,
+    updateConstraint,
+    deleteConstraint,
+  }
+
+  return ok([relationship, foreignKeyConstraint])
+}
+
+const constraintToCheckConstraint = (
+  columnName: string,
+  constraint: Constraint,
+  rawSql: string,
+): Result<CheckConstraint, UnexpectedTokenWarningError> => {
+  if (constraint.contype !== 'CONSTR_CHECK') {
+    return err(
+      new UnexpectedTokenWarningError('contype "CONSTR_CHECK" is expected'),
+    )
+  }
+
+  if (constraint.location === undefined) {
+    return err(new UnexpectedTokenWarningError('Invalid check constraint'))
+  }
+  let openParenthesesCount = 0
+  let endLocation: number | undefined = undefined
+  for (let i = constraint.location; i < rawSql.length; i++) {
+    if (rawSql[i] === '(') openParenthesesCount++
+    else if (rawSql[i] === ')') {
+      openParenthesesCount--
+      if (openParenthesesCount === 0) {
+        endLocation = i
+        break
+      }
+    }
+  }
path: frontend/packages/db-structure/src/parser/sql/postgresql/converter.ts
user: login: tnyo43
body: I had tried to use [pgsql-deparser](https://www.npmjs.com/package/pgsql-deparser) package to get Check constraint once but I gave up with it.

When I execute deparse with an object created by parsing `CHECK (price > 0)` by running `new Deparser({}, {}).Constraint(constraint)`, it throws `"TypeError: Cannot read properties of undefined (reading 'String')"` error.
When I execute deparse with an object created by parsing `CHECK (price > price)` (it will be false always but valid syntax),  it returns `"CHECK (    )"` which is an unexpected result.

I think something went wrong with either parsering or deparsering. (It might be same issue with https://github.com/launchql/pgsql-parser/issues/131)

I can investigate what's the matter f you are very interested in this problem, but maybe in another issue.

created_at: 2025-05-05T04:12:36Z
author_association: MEMBER
diff_hunk: @@ -97,11 +105,65 @@ const constraintToRelationship = (
     cardinality,
     updateConstraint,
     deleteConstraint,
-  })
+  }
+  const foreignKeyConstraint: ForeignKeyConstraint = {
+    type: 'FOREIGN KEY',
+    name,
+    columnName: foreignColumnName,
+    targetTableName: primaryTableName,
+    targetColumnName: primaryColumnName,
+    updateConstraint,
+    deleteConstraint,
+  }
+
+  return ok([relationship, foreignKeyConstraint])
+}
+
+const constraintToCheckConstraint = (
+  columnName: string,
+  constraint: Constraint,
+  rawSql: string,
+): Result<CheckConstraint, UnexpectedTokenWarningError> => {
+  if (constraint.contype !== 'CONSTR_CHECK') {
+    return err(
+      new UnexpectedTokenWarningError('contype "CONSTR_CHECK" is expected'),
+    )
+  }
+
+  if (constraint.location === undefined) {
+    return err(new UnexpectedTokenWarningError('Invalid check constraint'))
+  }
+  let openParenthesesCount = 0
+  let endLocation: number | undefined = undefined
+  for (let i = constraint.location; i < rawSql.length; i++) {
+    if (rawSql[i] === '(') openParenthesesCount++
+    else if (rawSql[i] === ')') {
+      openParenthesesCount--
+      if (openParenthesesCount === 0) {
+        endLocation = i
+        break
+      }
+    }
+  }
path: frontend/packages/db-structure/src/parser/sql/postgresql/converter.ts
user: login: MH4GF
body: I thought it was a practical solution to extract the values from rawSql. LGTM 👍🏻 
created_at: 2025-05-07T10:15:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -246,6 +311,55 @@ export const convertToSchema = (stmts: RawStmt[]): ProcessResult => {
     comment: string | null
   }
 
+  // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: <explanation>
path: frontend/packages/db-structure/src/parser/sql/postgresql/converter.ts
user: login: tnyo43
body: The index of complexity of this function is 16 while the threshold is 15. I gave up to reduce it to matches the threshold 😅 

I think the parsers' functions tend to be more complex than the modules of other packages. It might be possible to make the threshold higher for this package 🤔 
created_at: 2025-05-10T07:54:10Z
author_association: MEMBER
diff_hunk: @@ -246,6 +311,55 @@ export const convertToSchema = (stmts: RawStmt[]): ProcessResult => {
     comment: string | null
   }
 
+  // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: <explanation>
path: frontend/packages/db-structure/src/parser/sql/postgresql/converter.ts
user: login: MH4GF
body: You are right, it would be difficult to be compliant with a parser. Can you create a PR to change the settings?
created_at: 2025-05-12T00:53:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -97,11 +105,65 @@ const constraintToRelationship = (
     cardinality,
     updateConstraint,
     deleteConstraint,
-  })
+  }
+  const foreignKeyConstraint: ForeignKeyConstraint = {
+    type: 'FOREIGN KEY',
+    name,
+    columnName: foreignColumnName,
+    targetTableName: primaryTableName,
+    targetColumnName: primaryColumnName,
+    updateConstraint,
+    deleteConstraint,
+  }
+
+  return ok([relationship, foreignKeyConstraint])
+}
+
+const constraintToCheckConstraint = (
+  columnName: string,
+  constraint: Constraint,
+  rawSql: string,
+): Result<CheckConstraint, UnexpectedTokenWarningError> => {
+  if (constraint.contype !== 'CONSTR_CHECK') {
+    return err(
+      new UnexpectedTokenWarningError('contype "CONSTR_CHECK" is expected'),
+    )
+  }
+
+  if (constraint.location === undefined) {
+    return err(new UnexpectedTokenWarningError('Invalid check constraint'))
+  }
+  let openParenthesesCount = 0
+  let endLocation: number | undefined = undefined
+  for (let i = constraint.location; i < rawSql.length; i++) {
+    if (rawSql[i] === '(') openParenthesesCount++
+    else if (rawSql[i] === ')') {
+      openParenthesesCount--
+      if (openParenthesesCount === 0) {
+        endLocation = i
+        break
+      }
+    }
+  }
path: frontend/packages/db-structure/src/parser/sql/postgresql/converter.ts
user: login: pyramation
body: we may be able to deparse it with the new one! @tnyo43 

please try the latest, tag me if you have issues, I am happy to help.

I was heads down building the new version 🙌🏻
created_at: 2025-06-23T04:00:13Z
author_association: NONE

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-05-05T03:11:49Z
body: ###  🦋  Changeset detected

Latest commit: 7ad5442065e3c68dd06730aaa428ad7c2537eb80

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

<details><summary>This PR includes changesets to release 2 packages</summary>

  | Name                  | Type  |
| --------------------- | ----- |
| @liam-hq/db-structure | Patch |
| @liam-hq/cli          | Patch |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/liam-hq/liam/new/feat/postgresql-constraints?filename=.changeset/tiny-lies-agree.md&value=---%0A%22%40liam-hq%2Fdb-structure%22%3A%20patch%0A---%0A%0A%E2%9C%A8%20Enhance%20the%20PostgreSQL%20parser%20to%20support%20Constraints%0A)


author_association: NONE
user: login: vercel[bot]
created_at: 2025-05-05T03:11:50Z
body: [vc]: #URF96Km1adGYmJ7YpNhOfwTW7D65kZl99vGyUE+Q29s=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJsaWFtLWRvY3MiLCJyb290RGlyZWN0b3J5IjoiZnJvbnRlbmQvYXBwcy9kb2NzIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6IiJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vcm91dGUtMDYtY29yZS9saWFtLWRvY3MvNk1UdnhNZ1AxcFRhSEtTdjRyWmg2ZU1rOWlpaCIsIm5leHRDb21taXRTdGF0dXMiOiJJR05PUkVEIiwicHJldmlld1VybCI6ImxpYW0tZG9jcy1naXQtZmVhdC1wb3N0Z3Jlc3FsLWNvbnN0cmFpbnRzLXJvdXRlLTA2LWNvcmUudmVyY2VsLmFwcCJ9LHsibmFtZSI6ImxpYW0tZXJkLXNhbXBsZSIsInJvb3REaXJlY3RvcnkiOiJmcm9udGVuZC9hcHBzL2VyZC1zYW1wbGUiLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vcm91dGUtMDYtY29yZS9saWFtLWVyZC1zYW1wbGUvM1pqOTRraHlGcFBDUzhVUnd3ZVlFQ1FVVXdqQyIsInByZXZpZXdVcmwiOiJsaWFtLWVyZC1zYW1wbGUtZ2l0LWZlYXQtcG9zdGdyZXNxbC1jb25zdHJhaW50cy1yb3V0ZS0wNi1jb3JlLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoibGlhbS1lcmQtc2FtcGxlLWdpdC1mZWF0LXBvc3RncmVzcWwtY29uc3RyYWludHMtcm91dGUtMDYtY29yZS52ZXJjZWwuYXBwIn19LHsibmFtZSI6ImxpYW0tYXBwIiwicm9vdERpcmVjdG9yeSI6ImZyb250ZW5kL2FwcHMvYXBwIiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3JvdXRlLTA2LWNvcmUvbGlhbS1hcHAvSjduUTJveXIzMWR1WFBkM0VldFZaWHRFc2Y4biIsInByZXZpZXdVcmwiOiJsaWFtLWFwcC1naXQtZmVhdC1wb3N0Z3Jlc3FsLWNvbnN0cmFpbnRzLXJvdXRlLTA2LWNvcmUudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJsaWFtLWFwcC1naXQtZmVhdC1wb3N0Z3Jlc3FsLWNvbnN0cmFpbnRzLXJvdXRlLTA2LWNvcmUudmVyY2VsLmFwcCJ9fSx7Im5hbWUiOiJsaWFtLXN0b3J5Ym9vayIsInJvb3REaXJlY3RvcnkiOiJmcm9udGVuZC9pbnRlcm5hbC1wYWNrYWdlcy9zdG9yeWJvb2siLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vcm91dGUtMDYtY29yZS9saWFtLXN0b3J5Ym9vay95QUFxdnpoaHFMRUR0NXdrVlVFRUtnWURSZnV6IiwicHJldmlld1VybCI6ImxpYW0tc3Rvcnlib29rLWdpdC1mZWF0LXBvc3RncmVzcWwtY29uc3RyYWludHMtcm91dGUtMDYtY29yZS52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6ImxpYW0tc3Rvcnlib29rLWdpdC1mZWF0LXBvc3RncmVzcWwtY29uc3RyYWludHMtcm91dGUtMDYtY29yZS52ZXJjZWwuYXBwIn19XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **liam-app** | ✅ Ready ([Inspect](https://vercel.com/route-06-core/liam-app/J7nQ2oyr31duXPd3EetVZXtEsf8n)) | [Visit Preview](https://liam-app-git-feat-postgresql-constraints-route-06-core.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/liam-app-git-feat-postgresql-constraints-route-06-core.vercel.app?via=pr-comment-feedback-link) | May 10, 2025 7:54am |
| **liam-erd-sample** | ✅ Ready ([Inspect](https://vercel.com/route-06-core/liam-erd-sample/3Zj94khyFpPCS8URwweYECQUUwjC)) | [Visit Preview](https://liam-erd-sample-git-feat-postgresql-constraints-route-06-core.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/liam-erd-sample-git-feat-postgresql-constraints-route-06-core.vercel.app?via=pr-comment-feedback-link) | May 10, 2025 7:54am |
| **liam-storybook** | ✅ Ready ([Inspect](https://vercel.com/route-06-core/liam-storybook/yAAqvzhhqLEDt5wkVUEEKgYDRfuz)) | [Visit Preview](https://liam-storybook-git-feat-postgresql-constraints-route-06-core.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/liam-storybook-git-feat-postgresql-constraints-route-06-core.vercel.app?via=pr-comment-feedback-link) | May 10, 2025 7:54am |

<details><summary>1 Skipped Deployment</summary>

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **liam-docs** | ⬜️ Ignored ([Inspect](https://vercel.com/route-06-core/liam-docs/6MTvxMgP1pTaHKSv4rZh6eMk9iih)) | [Visit Preview](https://liam-docs-git-feat-postgresql-constraints-route-06-core.vercel.app) |  | May 10, 2025 7:54am |
</details>





author_association: NONE
user: login: supabase[bot]
created_at: 2025-05-05T03:11:56Z
body: [supa]:bnwmjoidqpnbpdxnsyqi
**Updates to [Preview Branch (feat/postgresql-constraints) ↗︎](https://supabase.com/dashboard/project/bnwmjoidqpnbpdxnsyqi)**

| Deployments | Status                      | Updated                   |
| ----------- | --------------------------- | ------------------------- |
| Database    | ✅ | Sat, 10 May 2025 07:50:23 UTC |
| Services    | ✅  | Sat, 10 May 2025 07:50:23 UTC  |
| APIs        | ✅      | Sat, 10 May 2025 07:50:23 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, 10 May 2025 07:50:23 UTC |
| Migrations     | ✅ | Sat, 10 May 2025 07:50:36 UTC |
| Seeding        | ✅   | Sat, 10 May 2025 07:50:37 UTC   |
| Edge Functions | ✅  | Sat, 10 May 2025 07:50:37 UTC  |

<hr/>




View logs for this [Workflow Run ↗︎](https://supabase.com/dashboard/project/bnwmjoidqpnbpdxnsyqi/branches).
Learn more about [Supabase for Git ↗︎](https://supabase.com/docs/guides/platform/branching).

author_association: NONE
user: login: qodo-free-for-open-source-projects[bot]
created_at: 2025-05-05T03:29:25Z
body: ## CI Feedback 🧐

#### (Feedback updated until commit https://github.com/liam-hq/liam/commit/342d6e009505a29ce3b09e141ad5863338213297)


A test triggered by this PR failed. Here is an AI-generated analysis of the failure:

<table><tr><td>

**Action:** e2e-tests (chromium)</td></tr>
<tr><td>

**Failed stage:** [Run e2e tests](https://github.com/liam-hq/liam/actions/runs/14828458268/job/41626613680) [❌]

</td></tr>
<tr><td>

**Failed test name:** tests/vrt/vrt.test.ts › top


</td></tr>
<tr><td>

**Failure summary:**

The action failed because a visual regression test (VRT) failed. Specifically, the "top" test in <br>tests/vrt/vrt.test.ts (line 24) failed because the actual screenshot did not match the expected <br>screenshot. The test reported:<br> <br> <li> "117 pixels (ratio 0.01 of all image pixels) are different" in most retry attempts<br> <li> "171 pixels (ratio 0.01 of all image pixels) are different" in some retry attempts<br> <br> The test was retried 5 times but continued to fail with the same screenshot comparison issue. The <br>failure occurred at line 11:22 in the screenshot function and at line 25:3 in the test file.<br>

</td></tr>
<tr><td>

<details><summary>Relevant error logs:</summary>


```yaml
1:  ##[group]Operating System
2:  Ubuntu
...

91:  ##[endgroup]
92:  [command]/usr/bin/git sparse-checkout disable
93:  [command]/usr/bin/git config --local --unset-all extensions.worktreeConfig
94:  ##[group]Checking out the ref
95:  [command]/usr/bin/git checkout --progress --force 342d6e009505a29ce3b09e141ad5863338213297
96:  Note: switching to '342d6e009505a29ce3b09e141ad5863338213297'.
97:  You are in 'detached HEAD' state. You can look around, make experimental
98:  changes and commit them, and you can discard any commits you make in this
99:  state without impacting any branches by switching back to a branch.
100:  If you want to create a new branch to retain commits you create, you may
101:  do so (now or later) by using -c with the switch command. Example:
102:  git switch -c <new-branch-name>
103:  Or undo this operation with:
104:  git switch -
105:  Turn off this advice by setting config variable advice.detachedHead to false
106:  HEAD is now at 342d6e0 🔧 fix error message
107:  ##[endgroup]
...

184:  CI: true
185:  URL: https://liam-o391cmrdi-route-06-core.vercel.app
186:  ENVIRONMENT: Preview – liam-app
187:  PNPM_HOME: /home/runner/setup-pnpm/node_modules/.bin
188:  ##[endgroup]
189:  Node version file is not JSON file
190:  Resolved ./.node-version as 22.15.0
191:  Found in cache @ /opt/hostedtoolcache/node/22.15.0/x64
192:  ##[group]Environment details
193:  node: v22.15.0
194:  npm: 10.9.2
195:  yarn: 1.22.22
196:  ##[endgroup]
197:  [command]/home/runner/setup-pnpm/node_modules/.bin/pnpm store path --silent
198:  /home/runner/setup-pnpm/node_modules/.bin/store/v10
199:  ##[warning]Failed to restore: Failed to GetCacheEntryDownloadURL: Received non-retryable error: Failed request: (403) Forbidden: unable to access resource in current scope
200:  pnpm cache is not found
...

211:  Lockfile is up to date, resolution step is skipped
212:  Progress: resolved 1, reused 0, downloaded 0, added 0
213:  Packages: +1637
214:  ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
215:  Progress: resolved 1637, reused 0, downloaded 180, added 174
216:  Progress: resolved 1637, reused 0, downloaded 307, added 287
217:  Progress: resolved 1637, reused 0, downloaded 526, added 519
218:  Progress: resolved 1637, reused 0, downloaded 596, added 551
219:  Progress: resolved 1637, reused 0, downloaded 618, added 563
220:  Progress: resolved 1637, reused 0, downloaded 786, added 778
221:  Progress: resolved 1637, reused 0, downloaded 975, added 952
222:  Progress: resolved 1637, reused 0, downloaded 1177, added 1190
223:  Progress: resolved 1637, reused 0, downloaded 1387, added 1378
224:  Progress: resolved 1637, reused 0, downloaded 1575, added 1575
225:  Progress: resolved 1637, reused 0, downloaded 1624, added 1637, done
226:  WARN  Failed to create bin at /home/runner/work/liam/liam/node_modules/.pnpm/node_modules/.bin/supabase. ENOENT: no such file or directory, open '/home/runner/work/liam/liam/node_modules/.pnpm/node_modules/supabase/bin/supabase'
227:  WARN  Failed to create bin at /home/runner/work/liam/liam/node_modules/.pnpm/supabase@2.22.3/node_modules/supabase/node_modules/.bin/supabase. ENOENT: no such file or directory, open '/home/runner/work/liam/liam/node_modules/.pnpm/supabase@2.22.3/node_modules/supabase/bin/supabase'
228:  .../node_modules/supabase postinstall$ node scripts/postinstall.js
229:  .../node_modules/supabase postinstall: Downloading https://github.com/supabase/cli/releases/download/v2.22.3/supabase_2.22.3_checksums.txt
230:  .../node_modules/supabase postinstall: Downloading https://github.com/supabase/cli/releases/download/v2.22.3/supabase_linux_amd64.tar.gz
231:  .../node_modules/supabase postinstall: Checksum verified.
232:  .../node_modules/supabase postinstall: Installed Supabase CLI successfully
233:  .../node_modules/supabase postinstall: Done
234:  WARN  Failed to create bin at /home/runner/work/liam/liam/frontend/apps/erd-sample/node_modules/.bin/liam. ENOENT: no such file or directory, open '/home/runner/work/liam/liam/frontend/packages/cli/dist-cli/bin/cli.js'
235:  devDependencies:
...

248:  │                                                                              │
249:  │   Ignored build scripts: @biomejs/biome, @bundled-es-modules/glob,           │
250:  │   @depot/cli, @prisma/client, @prisma/engines, @sentry/cli, core-js-pure,    │
251:  │   esbuild, protobufjs, sharp, style-dictionary.                              │
252:  │   Run "pnpm approve-builds" to pick which dependencies should be allowed     │
253:  │   to run scripts.                                                            │
254:  │                                                                              │
255:  ╰──────────────────────────────────────────────────────────────────────────────╯
256:  frontend/apps/docs postinstall$ fumadocs-mdx
257:  frontend/packages/jobs postinstall$ cp ../db-structure/node_modules/@ruby/prism/src/prism.wasm prism.wasm
258:  frontend/packages/jobs postinstall: Done
259:  frontend/apps/docs postinstall: [MDX] types generated
260:  frontend/apps/docs postinstall: Done
261:  frontend/apps/app postinstall$ cp ../../packages/db-structure/node_modules/@ruby/prism/src/prism.wasm prism.wasm
262:  frontend/apps/app postinstall: Done
263:  WARN  Failed to create bin at /home/runner/work/liam/liam/frontend/apps/erd-sample/node_modules/.bin/liam. ENOENT: no such file or directory, open '/home/runner/work/liam/liam/frontend/apps/erd-sample/node_modules/@liam-hq/cli/dist-cli/bin/cli.js'
264:  Done in 14.7s using pnpm v10.8.1
...

266:  with:
267:  path: ~/.cache/ms-playwright
268:  key: playwright-Linux-e3bcfe97d9dc7ef65fc9314d2715e0edc80fdee2ef2c86625f0d2c4171398564
269:  restore-keys: playwright-Linux-
270:  
271:  enableCrossOsArchive: false
272:  fail-on-cache-miss: false
273:  lookup-only: false
274:  save-always: false
275:  env:
276:  CI: true
277:  URL: https://liam-o391cmrdi-route-06-core.vercel.app
278:  ENVIRONMENT: Preview – liam-app
279:  PNPM_HOME: /home/runner/setup-pnpm/node_modules/.bin
280:  ##[endgroup]
281:  [warning]Event Validation Error: The event type deployment_status is not supported because it's not tied to a branch or tag ref.
282:  ##[group]Run pnpm exec playwright install --with-deps
...

1519:  |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■        |  90% of 2.3 MiB
1520:  |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■| 100% of 2.3 MiB
1521:  FFMPEG playwright build v1011 downloaded to /home/runner/.cache/ms-playwright/ffmpeg-1011
1522:  ##[group]Run pnpm exec playwright test --project="chromium"
1523:  [36;1mpnpm exec playwright test --project="chromium"[0m
1524:  shell: /usr/bin/bash -e {0}
1525:  env:
1526:  CI: true
1527:  URL: https://liam-o391cmrdi-route-06-core.vercel.app
1528:  ENVIRONMENT: Preview – liam-app
1529:  PNPM_HOME: /home/runner/setup-pnpm/node_modules/.bin
1530:  ##[endgroup]
1531:  Running 17 tests using 1 worker
1532:  ················×××××F
1533:  1) [chromium] › tests/vrt/vrt.test.ts:24:5 › top ─────────────────────────────────────────────────
1534:  Error: [2mexpect([22m[31mpage[39m[2m).[22mtoHaveScreenshot[2m([22m[32mexpected[39m[2m)[22m
1535:  117 pixels (ratio 0.01 of all image pixels) are different.
...

1557:  |                      ^
1558:  12 | }
1559:  13 |
1560:  14 | interface TargetPage {
1561:  at screenshot (/home/runner/work/liam/liam/frontend/packages/e2e/tests/vrt/vrt.test.ts:11:22)
1562:  at /home/runner/work/liam/liam/frontend/packages/e2e/tests/vrt/vrt.test.ts:25:3
1563:  attachment #1: top-1-expected.png (image/png) ──────────────────────────────────────────────────
1564:  tests/vrt/vrt.test.ts-snapshots/top-1-chromium-linux.png
1565:  ────────────────────────────────────────────────────────────────────────────────────────────────
1566:  attachment #2: top-1-actual.png (image/png) ────────────────────────────────────────────────────
1567:  test-results/vrt-vrt-top-chromium/top-1-actual.png
1568:  ────────────────────────────────────────────────────────────────────────────────────────────────
1569:  attachment #3: top-1-diff.png (image/png) ──────────────────────────────────────────────────────
1570:  test-results/vrt-vrt-top-chromium/top-1-diff.png
1571:  ────────────────────────────────────────────────────────────────────────────────────────────────
1572:  Error Context: test-results/vrt-vrt-top-chromium/error-context.md
1573:  Retry #1 ───────────────────────────────────────────────────────────────────────────────────────
1574:  Error: [2mexpect([22m[31mpage[39m[2m).[22mtoHaveScreenshot[2m([22m[32mexpected[39m[2m)[22m
1575:  117 pixels (ratio 0.01 of all image pixels) are different.
...

1597:  |                      ^
1598:  12 | }
1599:  13 |
1600:  14 | interface TargetPage {
1601:  at screenshot (/home/runner/work/liam/liam/frontend/packages/e2e/tests/vrt/vrt.test.ts:11:22)
1602:  at /home/runner/work/liam/liam/frontend/packages/e2e/tests/vrt/vrt.test.ts:25:3
1603:  attachment #1: top-1-expected.png (image/png) ──────────────────────────────────────────────────
1604:  tests/vrt/vrt.test.ts-snapshots/top-1-chromium-linux.png
1605:  ────────────────────────────────────────────────────────────────────────────────────────────────
1606:  attachment #2: top-1-actual.png (image/png) ────────────────────────────────────────────────────
1607:  test-results/vrt-vrt-top-chromium-retry1/top-1-actual.png
1608:  ────────────────────────────────────────────────────────────────────────────────────────────────
1609:  attachment #3: top-1-diff.png (image/png) ──────────────────────────────────────────────────────
1610:  test-results/vrt-vrt-top-chromium-retry1/top-1-diff.png
1611:  ────────────────────────────────────────────────────────────────────────────────────────────────
1612:  Error Context: test-results/vrt-vrt-top-chromium-retry1/error-context.md
1613:  attachment #5: trace (application/zip) ─────────────────────────────────────────────────────────
1614:  test-results/vrt-vrt-top-chromium-retry1/trace.zip
1615:  Usage:
1616:  pnpm exec playwright show-trace test-results/vrt-vrt-top-chromium-retry1/trace.zip
1617:  ────────────────────────────────────────────────────────────────────────────────────────────────
1618:  Retry #2 ───────────────────────────────────────────────────────────────────────────────────────
1619:  Error: [2mexpect([22m[31mpage[39m[2m).[22mtoHaveScreenshot[2m([22m[32mexpected[39m[2m)[22m
1620:  117 pixels (ratio 0.01 of all image pixels) are different.
...

1642:  |                      ^
1643:  12 | }
1644:  13 |
1645:  14 | interface TargetPage {
1646:  at screenshot (/home/runner/work/liam/liam/frontend/packages/e2e/tests/vrt/vrt.test.ts:11:22)
1647:  at /home/runner/work/liam/liam/frontend/packages/e2e/tests/vrt/vrt.test.ts:25:3
1648:  attachment #1: top-1-expected.png (image/png) ──────────────────────────────────────────────────
1649:  tests/vrt/vrt.test.ts-snapshots/top-1-chromium-linux.png
1650:  ────────────────────────────────────────────────────────────────────────────────────────────────
1651:  attachment #2: top-1-actual.png (image/png) ────────────────────────────────────────────────────
1652:  test-results/vrt-vrt-top-chromium-retry2/top-1-actual.png
1653:  ────────────────────────────────────────────────────────────────────────────────────────────────
1654:  attachment #3: top-1-diff.png (image/png) ──────────────────────────────────────────────────────
1655:  test-results/vrt-vrt-top-chromium-retry2/top-1-diff.png
1656:  ────────────────────────────────────────────────────────────────────────────────────────────────
1657:  Error Context: test-results/vrt-vrt-top-chromium-retry2/error-context.md
1658:  Retry #3 ───────────────────────────────────────────────────────────────────────────────────────
1659:  Error: [2mexpect([22m[31mpage[39m[2m).[22mtoHaveScreenshot[2m([22m[32mexpected[39m[2m)[22m
1660:  171 pixels (ratio 0.01 of all image pixels) are different.
...

1682:  |                      ^
1683:  12 | }
1684:  13 |
1685:  14 | interface TargetPage {
1686:  at screenshot (/home/runner/work/liam/liam/frontend/packages/e2e/tests/vrt/vrt.test.ts:11:22)
1687:  at /home/runner/work/liam/liam/frontend/packages/e2e/tests/vrt/vrt.test.ts:25:3
1688:  attachment #1: top-1-expected.png (image/png) ──────────────────────────────────────────────────
1689:  tests/vrt/vrt.test.ts-snapshots/top-1-chromium-linux.png
1690:  ────────────────────────────────────────────────────────────────────────────────────────────────
1691:  attachment #2: top-1-actual.png (image/png) ────────────────────────────────────────────────────
1692:  test-results/vrt-vrt-top-chromium-retry3/top-1-actual.png
1693:  ────────────────────────────────────────────────────────────────────────────────────────────────
1694:  attachment #3: top-1-diff.png (image/png) ──────────────────────────────────────────────────────
1695:  test-results/vrt-vrt-top-chromium-retry3/top-1-diff.png
1696:  ────────────────────────────────────────────────────────────────────────────────────────────────
1697:  Error Context: test-results/vrt-vrt-top-chromium-retry3/error-context.md
1698:  Retry #4 ───────────────────────────────────────────────────────────────────────────────────────
1699:  Error: [2mexpect([22m[31mpage[39m[2m).[22mtoHaveScreenshot[2m([22m[32mexpected[39m[2m)[22m
1700:  117 pixels (ratio 0.01 of all image pixels) are different.
...

1722:  |                      ^
1723:  12 | }
1724:  13 |
1725:  14 | interface TargetPage {
1726:  at screenshot (/home/runner/work/liam/liam/frontend/packages/e2e/tests/vrt/vrt.test.ts:11:22)
1727:  at /home/runner/work/liam/liam/frontend/packages/e2e/tests/vrt/vrt.test.ts:25:3
1728:  attachment #1: top-1-expected.png (image/png) ──────────────────────────────────────────────────
1729:  tests/vrt/vrt.test.ts-snapshots/top-1-chromium-linux.png
1730:  ────────────────────────────────────────────────────────────────────────────────────────────────
1731:  attachment #2: top-1-actual.png (image/png) ────────────────────────────────────────────────────
1732:  test-results/vrt-vrt-top-chromium-retry4/top-1-actual.png
1733:  ────────────────────────────────────────────────────────────────────────────────────────────────
1734:  attachment #3: top-1-diff.png (image/png) ──────────────────────────────────────────────────────
1735:  test-results/vrt-vrt-top-chromium-retry4/top-1-diff.png
1736:  ────────────────────────────────────────────────────────────────────────────────────────────────
1737:  Error Context: test-results/vrt-vrt-top-chromium-retry4/error-context.md
1738:  Retry #5 ───────────────────────────────────────────────────────────────────────────────────────
1739:  Error: [2mexpect([22m[31mpage[39m[2m).[22mtoHaveScreenshot[2m([22m[32mexpected[39m[2m)[22m
1740:  171 pixels (ratio 0.01 of all image pixels) are different.
...

1762:  |                      ^
1763:  12 | }
1764:  13 |
1765:  14 | interface TargetPage {
1766:  at screenshot (/home/runner/work/liam/liam/frontend/packages/e2e/tests/vrt/vrt.test.ts:11:22)
1767:  at /home/runner/work/liam/liam/frontend/packages/e2e/tests/vrt/vrt.test.ts:25:3
1768:  attachment #1: top-1-expected.png (image/png) ──────────────────────────────────────────────────
1769:  tests/vrt/vrt.test.ts-snapshots/top-1-chromium-linux.png
1770:  ────────────────────────────────────────────────────────────────────────────────────────────────
1771:  attachment #2: top-1-actual.png (image/png) ────────────────────────────────────────────────────
1772:  test-results/vrt-vrt-top-chromium-retry5/top-1-actual.png
1773:  ────────────────────────────────────────────────────────────────────────────────────────────────
1774:  attachment #3: top-1-diff.png (image/png) ──────────────────────────────────────────────────────
1775:  test-results/vrt-vrt-top-chromium-retry5/top-1-diff.png
1776:  ────────────────────────────────────────────────────────────────────────────────────────────────
1777:  Error Context: test-results/vrt-vrt-top-chromium-retry5/error-context.md
1778:  1 failed
1779:  [chromium] › tests/vrt/vrt.test.ts:24:5 › top ──────────────────────────────────────────────────
1780:  16 passed (1.5m)
1781:  ##[error]Process completed with exit code 1.
1782:  ##[group]Run actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02

```
</details></td></tr></table>
author_association: CONTRIBUTOR
user: login: tnyo43
created_at: 2025-05-05T06:55:23Z
body: [The e2e test fails](https://github.com/liam-hq/liam/actions/runs/14828458268/job/41626613680?pr=1587).
But [results of the e2e test look flaky](https://github.com/liam-hq/liam/actions/workflows/e2e_tests.yml) and this change should not affect its result (the application uses schema.rb parser for the snapshots).

Could you please help me to solve this problem? I guess it's better to take some actions but not in this PR.
author_association: MEMBER
user: login: qodo-free-for-open-source-projects[bot]
created_at: 2025-05-05T06:56:19Z
body: ## PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

<table>
<tr><td>⏱️&nbsp;<strong>Estimated effort to review</strong>: 3 🔵🔵🔵⚪⚪</td></tr>
<tr><td>🧪&nbsp;<strong>PR contains tests</strong></td></tr>
<tr><td>🔒&nbsp;<strong>No security concerns identified</strong></td></tr>
<tr><td>⚡&nbsp;<strong>Recommended focus areas for review</strong><br><br>

<details><summary><a href='https://github.com/liam-hq/liam/pull/1587/files#diff-bae74cb1e5472bf6f333ebc48a79fe05fa5d0b74268d5cd067967896ef0f2cb8R133-R151'><strong>Error Handling</strong></a>

The constraintToCheckConstraint function has limited error handling for parsing check constraints. If the SQL contains malformed check constraints, the current implementation might not handle all edge cases correctly.
</summary>

```typescript
if (constraint.location === undefined) {
  return err(new UnexpectedTokenWarningError('Invalid check constraint'))
}
let openParenthesesCount = 0
let endLocation: number | undefined = undefined
for (let i = constraint.location; i < rawSql.length; i++) {
  if (rawSql[i] === '(') openParenthesesCount++
  else if (rawSql[i] === ')') {
    openParenthesesCount--
    if (openParenthesesCount === 0) {
      endLocation = i
      break
    }
  }
}

if (endLocation === undefined) {
  return err(new UnexpectedTokenWarningError('Invalid check constraint'))
}
```

</details>

<details><summary><a href='https://github.com/liam-hq/liam/pull/1587/files#diff-bae74cb1e5472bf6f333ebc48a79fe05fa5d0b74268d5cd067967896ef0f2cb8R207-R256'><strong>Code Duplication</strong></a>

There's some duplication in constraint handling logic between column-level constraints and table-level constraints. Consider refactoring to use shared functions for processing different constraint types.
</summary>

```typescript
let isPrimary = false
let isUnique = false
for (const constraint of colDef.constraints ?? []) {
  if (!isConstraintNode(constraint)) continue

  if (constraint.Constraint.contype === 'CONSTR_PRIMARY') {
    const constraintName = `PRIMARY_${colDef.colname}`
    constraints[constraintName] = {
      name: constraintName,
      type: 'PRIMARY KEY',
      columnName: colDef.colname,
    }
    isPrimary = true
    isUnique = true
  } else if (constraint.Constraint.contype === 'CONSTR_UNIQUE') {
    const constraintName = `UNIQUE_${colDef.colname}`
    constraints[constraintName] = {
      name: constraintName,
      type: 'UNIQUE',
      columnName: colDef.colname,
    }
    isUnique = true
  } else if (constraint.Constraint.contype === 'CONSTR_FOREIGN') {
    const relResult = constraintToRelationshipAndForeignKeyConstraint(
      tableName,
      colDef.colname,
      constraint.Constraint,
    )
    if (relResult.isErr()) {
      errors.push(relResult.error)
      continue
    }
    const [relationship, foreignKeyConstraint] = relResult.value

    relationships[relationship.name] = relationship
    constraints[foreignKeyConstraint.name] = foreignKeyConstraint
  } else if (constraint.Constraint.contype === 'CONSTR_CHECK') {
    const relResult = constraintToCheckConstraint(
      colDef.colname,
      constraint.Constraint,
      rawSql,
    )

    if (relResult.isErr()) {
      errors.push(relResult.error)
      continue
    }
    const checkConstraint = relResult.value
    constraints[checkConstraint.name] = checkConstraint
  }
```

</details>

</td></tr>
</table>

author_association: CONTRIBUTOR
user: login: qodo-free-for-open-source-projects[bot]
created_at: 2025-05-05T06:57:03Z
body: ## PR Code Suggestions ✨

<!-- 342d6e0 -->

Explore these optional code suggestions:

<table><thead><tr><td><strong>Category</strong></td><td align=left><strong>Suggestion&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </strong></td><td align=center><strong>Impact</strong></td></tr><tbody><tr><td rowspan=1>Learned<br>best practice</td>
<td>



<details><summary>Validate input parameters<!-- not_implemented --></summary>

___


**Add validation for the <code>rawSql</code> parameter at the beginning of the function. The <br>function relies on accessing characters in the rawSql string, but doesn't verify <br>that rawSql is a non-empty string before processing it, which could lead to <br>runtime errors.**

[frontend/packages/db-structure/src/parser/sql/postgresql/converter.ts [122-135]](https://github.com/liam-hq/liam/pull/1587/files#diff-bae74cb1e5472bf6f333ebc48a79fe05fa5d0b74268d5cd067967896ef0f2cb8R122-R135)

```diff
 const constraintToCheckConstraint = (
   columnName: string,
   constraint: Constraint,
   rawSql: string,
 ): Result<CheckConstraint, UnexpectedTokenWarningError> => {
+  if (!rawSql || typeof rawSql !== 'string') {
+    return err(new UnexpectedTokenWarningError('Invalid or missing SQL string'))
+  }
+  
   if (constraint.contype !== 'CONSTR_CHECK') {
     return err(
       new UnexpectedTokenWarningError('contype "CONSTR_CHECK" is expected'),
     )
   }
 
   if (constraint.location === undefined) {
     return err(new UnexpectedTokenWarningError('Invalid check constraint'))
   }
```

- [ ] **Apply this suggestion** <!-- /improve --apply_suggestion=0 -->

<details><summary>Suggestion importance[1-10]: 6</summary>

__

Why: 
Relevant best practice - Validate input data before processing to prevent runtime errors and unexpected behavior. Add explicit checks for required parameters, proper formats, or valid values at the beginning of functions.

</details></details></td><td align=center>Low

</td></tr><tr><td rowspan=1>General</td>
<td>



<details><summary>Remove redundant undefined check</summary>

___


**The code incorrectly assumes <code>relResult.value</code> can be both <code>undefined</code> and a tuple. <br>Since you've already checked for <code>undefined</code>, the destructuring will never execute <br>on an undefined value, making the check redundant. Remove the unnecessary check.**

[frontend/packages/db-structure/src/parser/sql/postgresql/converter.ts [441-442]](https://github.com/liam-hq/liam/pull/1587/files#diff-bae74cb1e5472bf6f333ebc48a79fe05fa5d0b74268d5cd067967896ef0f2cb8R441-R442)

```diff
-if (relResult.value === undefined) continue
 const [relationship, foreignKeyConstraint] = relResult.value
```

- [ ] **Apply this suggestion** <!-- /improve --apply_suggestion=1 -->

<details><summary>Suggestion importance[1-10]: 3</summary>

__

Why: The suggestion correctly identifies that the check `if (relResult.value === undefined)` is redundant because the preceding `if (relResult.isErr())` check already handles the case where `value` would be undefined. Removing the check slightly improves code conciseness.


</details></details></td><td align=center>Low

</td></tr>
<tr><td align="center" colspan="2">

- [ ] Update <!-- /improve --more_suggestions=true -->

</td><td></td></tr></tbody></table>
author_association: CONTRIBUTOR
user: login: FunamaYukina
created_at: 2025-05-07T07:16:03Z
body: Oops, sorry.
I merged the following pull request and had a conflict.🙏
- https://github.com/liam-hq/liam/pull/1583
author_association: MEMBER
user: login: tnyo43
created_at: 2025-05-10T07:56:22Z
body: @MH4GF @FunamaYukina 
Could you please review this again since I modified this PR since https://github.com/liam-hq/liam/pull/1583 is merged 🙏 
author_association: MEMBER