Skip to content

fix(cli): lowercase the postgres database name derived from the app name - #1330

Merged
vivek7405 merged 2 commits into
mainfrom
fix/pg-db-name-lowercase
Aug 8, 2026
Merged

fix(cli): lowercase the postgres database name derived from the app name#1330
vivek7405 merged 2 commits into
mainfrom
fix/pg-db-name-lowercase

Conversation

@vivek7405

@vivek7405 vivek7405 commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Closes #1255

webjs create MyApp --db postgres wrote the app name into the generated .env.example DATABASE_URL with a case-preserving replace (name.replace(/[^a-z0-9_]/gi, '_'), where the i flag is what let capitals through). PostgreSQL folds an unquoted identifier, so CREATE DATABASE MyApp; creates myapp and the emitted URL then fails with database "MyApp" does not exist. createdb MyApp goes the other way and creates MyApp literally, because it quotes through fmtId. So the real defect is broader than a wrong URL: the emitted name was not quoting-invariant, and which database you ended up with depended on which creation route you took.

What changed

  • packages/cli/lib/app-name.js: new pure exports toDatabaseName(name) and DB_NAME_MAX_LENGTH (63). The rule is fold with toLowerCase(), map every remaining non-[a-z0-9_] character to _ one for one, prefix a single _ when the result starts with a digit, then slice to 63. The order is load-bearing: the fold before the slice makes the byte cap exact (every surviving character is one ASCII byte), and the prefix before the slice makes the cap govern the final string. The module still imports nothing.
  • packages/cli/lib/create.js: hoists const dbName = toDatabaseName(name) and uses it in the DATABASE_URL line, and the post-scaffold Postgres guidance now names that database so the user knows what to create. The idempotent replace-or-append block below it is unchanged.

The app name itself is untouched everywhere else: the directory, the package.json name, the {{APP_NAME}} substitution, displayName, metadata.title. Only that one URL segment is normalized. The sqlite arm is a fixed file:./db/dev.db with no name interpolation, so it has no exposure.

Deliberately excluded, all argued in the issue: no reserved-word detection (the list is version-dependent and roughly 470 entries, and createdb user already works because fmtId quotes it; the JSDoc now states that carve-out explicitly, since a name folding to a keyword is the one case the quoting-invariance property does not cover), no collision detection (My-App and my_app both fold to my_app, but this is a placeholder in .env.example, not a provisioned resource, and a uniquifying suffix would make the name untraceable), no camel split (Rails splits, but the goal is the name PostgreSQL itself folds to, and a separator PostgreSQL would not insert is a mismatch in the other direction), and no empty-result fallback (scaffoldApp asserts checkAppName before any file is written, so a validated name's first character always survives the fold).

Test plan

  • Unit (packages/cli/test/app-name/app-name.test.js): a derivation table (MyApp to myapp, TaskFlow to taskflow with no split, My.App-2 to my_app_2, my-pg to my_pg unchanged, 2app to _2app), the 63 cap including the fold-before-slice and prefix-before-slice cases, idempotence, and a shape property asserting every name the existing accept-list declares valid derives a non-empty /^[a-z_][a-z0-9_]*$/ string. The accept-list is hoisted to VALID_NAMES so the property runs over exactly that corpus.
  • Scaffold integration (test/scaffolds/scaffold-integration.test.js): scaffolds MyPgApp and asserts the exact anchored line DATABASE_URL=postgres://user:password@localhost:5432/mypgapp plus that the printed guidance names mypgapp (via a new captureConsole() sibling to muteConsole(), so no existing caller changes), and scaffolds my-pg asserting the byte-identical-to-before .../my_pg.
  • Counterfactual, run: restoring the gi regex reds the MyPgApp assertion and leaves the my-pg one green. That asymmetry is what proves the test targets the bug rather than the mechanism.
  • Fresh-app generation: webjs create MyPgApp --db postgres --no-install emits DATABASE_URL=postgres://user:password@localhost:5432/mypgapp, the banner prints "The example URL names the database mypgapp", and webjs check inside the generated app passes. The --runtime bun variant emits a byte-identical .env.example.
  • npm test: 4064/4070 pass. The 5 failures are the elision differential tests and test/bun/listener.test.mjs, none of which this diff can reach. They are a worktree artifact: packages/core/dist there is a symlink to the primary checkout, and the same tests pass in the primary. CI builds from the branch, so it is unaffected.
  • Dogfood: the website boots in prod mode and serves 200 on /, /docs/database, /ui and /ui/button with no broken modulepreload hints. examples/blog is N/A: this diff is the webjs create scaffold writer and changes nothing either in-repo app serves.
  • Browser / e2e / smoke: N/A. The value lands in .env.example, which is never served, never imported, and never reaches a page, layout, or component. No route, request path, or navigation behaviour changes.
  • Bun parity: N/A. A pure ASCII string transform inside the CLI scaffold writer. No serializer, listener, dispatch, stream, node:crypto, or TS-stripper surface. The parity hook's keyword filter does not match create.js or app-name.js, and the --runtime bun generation check above covers the one bun-adjacent surface.

Docs

Surface Change
packages/cli/AGENTS.md toDatabaseName added to the app-name.js module-map entry with the quoting-invariance rationale; Tests: line extended.
website/app/docs/database/page.ts One sentence in "Switching to Postgres" on the derived name.
Root AGENTS.md One clause on the emitted Postgres DATABASE_URL appended to the scaffolding paragraph that already documents the uppercase carve-out.

Checked and deliberately not changed: .agents/skills/webjs/references/built-ins.md and website/app/docs/configuration/page.ts mention DATABASE_URL only as an env-var example, packages/cli/templates/.env.example carries the sqlite default that create.js writes over, packages/cli/lib/api-gallery.js mentions it only as an env.ts schema key. README and CONVENTIONS.md: N/A, not a headline capability and no convention changed.

MCP, editor plugins, marketing copy, scaffold templates, version bumps: N/A. No introspection surface, grammar, snippet, landing-page claim, or template content changes.

`webjs create MyApp --db postgres` wrote the app name into the emitted
DATABASE_URL with a case-preserving replace, so the URL named `MyApp`.
PostgreSQL folds an unquoted identifier, so `CREATE DATABASE MyApp;`
creates `myapp` and the emitted URL then fails to connect. `createdb
MyApp` goes the other way and creates `MyApp` literally, because it
quotes through fmtId. The real defect is that the emitted name was not
quoting-invariant, so which database you got depended on which creation
route you took.

Derive it once in a new pure `toDatabaseName()` in app-name.js: fold
with toLowerCase (never toLocaleLowerCase, which is locale-dependent),
map every remaining non-[a-z0-9_] character to `_` one for one, prefix a
single `_` when the result starts with a digit, then cap at 63 bytes,
PostgreSQL's NAMEDATALEN - 1. The order is load-bearing: the fold before
the slice makes the byte cap exact, and the prefix before the slice
makes the cap govern the final string.

The app name itself is untouched everywhere else. Only that one URL
segment changes, and the post-scaffold guidance now names the database
so the user knows what to create.
@vivek7405 vivek7405 self-assigned this Aug 8, 2026
A name folding to a keyword (order, user, table) is still not
quoting-invariant: CREATE DATABASE order; is a syntax error while
createdb order succeeds, because fmtId quotes a keyword. The JSDoc
stated the property without that qualification. Not detected on
purpose, and the comment now says why.

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Read the whole diff. The derivation is right and the fold-before-slice ordering is the part I wanted to check hardest, since that is what makes the 63 cap a byte cap rather than a code-unit one, and it holds. Hoisting dbName so the emitted URL and the printed guidance cannot drift is the right call, and I like that the no-regression case (my-pg still yields my_pg) is pinned as its own assertion rather than folded into the mixed-case one, because that asymmetry is what makes the counterfactual meaningful.

One real problem, on the JSDoc rather than the code: the quoting-invariance property is stated without qualification and there is a case it does not cover. Inline.

The thing I would keep an eye on is scope creep into reserved-word or collision handling. Both are genuinely out of scope here and the issue argues that well, so the right move is to say so in the comment rather than to start detecting anything.

Comment thread packages/cli/lib/app-name.js
@vivek7405
vivek7405 marked this pull request as ready for review August 8, 2026 12:57
@vivek7405
vivek7405 merged commit a9f88ca into main Aug 8, 2026
10 checks passed
@vivek7405
vivek7405 deleted the fix/pg-db-name-lowercase branch August 8, 2026 13:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(cli): lowercase the postgres database name derived from the app name

1 participant