Skip to content

✨ Enhance the PostgreSQL parser to support Constraints #1587

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Conversation

tnyo43
Copy link
Member

@tnyo43 tnyo43 commented May 5, 2025

Issue

  • resolve:

Why is this change needed?

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

ref

What would you like reviewers to focus on?

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

Testing Verification

  • About the parser, I updated some test cases
  • About the application, I checked the behaviour in the following steps
    1. copy the following schema and save on your local PC (that's a slight modified copy of this file)
    2. run pnpm run build --filter @liam-hq/cli to build cli
    3. run node frontend/packages/cli/dist-cli/bin/cli.js erd build --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
schema.in.sql
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.';
users posts
Screenshot 0007-05-05 at 12 31 27 Screenshot 0007-05-05 at 12 32 06

What was done

🤖 Generated by PR Agent at 342d6e0

  • 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

Relevant files
Enhancement
converter.ts
Add constraint extraction and storage to PostgreSQL schema converter

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

  • Add logic to extract PRIMARY KEY, UNIQUE, FOREIGN KEY, and CHECK
    constraints.
  • Refactor relationship extraction to also return foreign key
    constraints.
  • Implement parsing of CHECK constraint expressions from raw SQL.
  • Store constraints in the table schema output.
  • Update index and ALTER TABLE handling to preserve constraints.
  • +132/-44
    index.ts
    Update processor to support raw SQL for constraint parsing

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

  • Pass raw SQL to schema converter for CHECK constraint extraction.
  • Update processor to accommodate new converter signature.
  • +1/-0     
    Tests
    index.test.ts
    Add and update tests for constraint parsing                           

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

  • Add and update tests for PRIMARY KEY, UNIQUE, FOREIGN KEY, and CHECK
    constraints.
  • Assert that constraints are correctly parsed and included in output.
  • Enhance test coverage for new constraint logic.
  • +81/-1   
    Documentation
    olive-women-notice.md
    Add changeset for PostgreSQL constraint support                   

    .changeset/olive-women-notice.md

  • Add changeset describing enhancement of PostgreSQL parser to support
    constraints.
  • +5/-0     

    Additional Notes


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

    changeset-bot bot commented May 5, 2025

    🦋 Changeset detected

    Latest commit: 7ad5442

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

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

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

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

    Copy link

    vercel bot commented May 5, 2025

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

    Name Status Preview Comments Updated (UTC)
    liam-app ✅ Ready (Inspect) Visit Preview 💬 Add feedback May 10, 2025 7:54am
    liam-erd-sample ✅ Ready (Inspect) Visit Preview 💬 Add feedback May 10, 2025 7:54am
    liam-storybook ✅ Ready (Inspect) Visit Preview 💬 Add feedback May 10, 2025 7:54am
    1 Skipped Deployment
    Name Status Preview Comments Updated (UTC)
    liam-docs ⬜️ Ignored (Inspect) Visit Preview May 10, 2025 7:54am

    Copy link

    supabase bot commented May 5, 2025

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

    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

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

    Copy link
    Contributor

    qodo-merge-pro-for-open-source bot commented May 5, 2025

    CI Feedback 🧐

    (Feedback updated until commit 342d6e0)

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

    Action: e2e-tests (chromium)

    Failed stage: Run e2e tests [❌]

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

    Failure summary:

    The action failed because a visual regression test (VRT) failed. Specifically, the "top" test in
    tests/vrt/vrt.test.ts (line 24) failed because the actual screenshot did not match the expected
    screenshot. The test reported:

  • "117 pixels (ratio 0.01 of all image pixels) are different" in most retry attempts
  • "171 pixels (ratio 0.01 of all image pixels) are different" in some retry attempts

    The test was retried 5 times but continued to fail with the same screenshot comparison issue. The
    failure occurred at line 11:22 in the screenshot function and at line 25:3 in the test file.

  • Relevant error logs:
    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
    

    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 ?? []) {
    Copy link
    Member Author

    Choose a reason for hiding this comment

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

    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.

    export const convertToSchema = (stmts: RawStmt[]): ProcessResult => {
    export const convertToSchema = (
    stmts: RawStmt[],
    rawSql: string,
    Copy link
    Member Author

    @tnyo43 tnyo43 May 5, 2025

    Choose a reason for hiding this comment

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

    added this argument for constraintToCheckConstraint function.
    For the detail: #1587 (comment)

    Comment on lines -65 to +120
    return ok(undefined)
    return err(
    new UnexpectedTokenWarningError('contype "CONSTR_FOREIGN" is expected'),
    )
    Copy link
    Member Author

    Choose a reason for hiding this comment

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

    Because of this change, 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.

    Comment on lines 136 to 197
    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
    }
    }
    }
    Copy link
    Member Author

    Choose a reason for hiding this comment

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

    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

    Copy link
    Member Author

    Choose a reason for hiding this comment

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

    I had tried to use 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 launchql/pgsql-parser#131)

    I can investigate what's the matter f you are very interested in this problem, but maybe in another issue.

    Copy link
    Member

    Choose a reason for hiding this comment

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

    I thought it was a practical solution to extract the values from rawSql. LGTM 👍🏻

    @tnyo43
    Copy link
    Member Author

    tnyo43 commented May 5, 2025

    The e2e test fails.
    But results of the e2e test look flaky 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.

    @tnyo43 tnyo43 marked this pull request as ready for review May 5, 2025 06:55
    @tnyo43 tnyo43 requested a review from a team as a code owner May 5, 2025 06:55
    @tnyo43 tnyo43 requested review from hoshinotsuyoshi, FunamaYukina, junkisai, MH4GF and NoritakaIkeda and removed request for a team May 5, 2025 06:55
    Copy link
    Contributor

    PR Reviewer Guide 🔍

    Here are some key observations to aid the review process:

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

    Error Handling

    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.

    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'))
    }
    Code Duplication

    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.

    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
      }

    Copy link
    Contributor

    qodo-merge-pro-for-open-source bot commented May 5, 2025

    PR Code Suggestions ✨

    Explore these optional code suggestions:

    CategorySuggestion                                                                                                                                    Impact
    Learned
    best practice
    Validate input parameters

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

    frontend/packages/db-structure/src/parser/sql/postgresql/converter.ts [122-135]

     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
    Suggestion importance[1-10]: 6

    __

    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.

    Low
    General
    Remove redundant undefined check

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

    frontend/packages/db-structure/src/parser/sql/postgresql/converter.ts [441-442]

    -if (relResult.value === undefined) continue
     const [relationship, foreignKeyConstraint] = relResult.value
    • Apply this suggestion
    Suggestion importance[1-10]: 3

    __

    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.

    Low
    • Update

    @FunamaYukina
    Copy link
    Member

    Oops, sorry.
    I merged the following pull request and had a conflict.🙏

    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.

    This was more difficult problem than I thought. Thanks for implementing!

    Comment on lines 136 to 197
    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
    }
    }
    }
    Copy link
    Member

    Choose a reason for hiding this comment

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

    I thought it was a practical solution to extract the values from rawSql. LGTM 👍🏻

    tnyo43 added 3 commits May 8, 2025 20:14
    tnyo43 added 5 commits May 8, 2025 21:30
    to get constraint expressions, it refers to the original raw SQL texts and extract the expressions with constraint.location
    The processRelationshipsAndConstraints function is still more complex than the threshold, the score is 16 while it should be 15 or less.
    Copy link
    Member Author

    @tnyo43 tnyo43 left a comment

    Choose a reason for hiding this comment

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

    I modified it a little since #1583 is merged.

    @@ -246,6 +311,55 @@ export const convertToSchema = (stmts: RawStmt[]): ProcessResult => {
    comment: string | null
    }

    // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: <explanation>
    Copy link
    Member Author

    Choose a reason for hiding this comment

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

    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 🤔

    @tnyo43
    Copy link
    Member Author

    tnyo43 commented May 10, 2025

    @MH4GF @FunamaYukina
    Could you please review this again since I modified this PR since #1583 is merged 🙏

    @tnyo43 tnyo43 requested a review from MH4GF May 10, 2025 07:56
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
    Projects
    None yet
    Development

    Successfully merging this pull request may close these issues.

    3 participants