Skip to content

Default port for portless DATABASE_URLs - #5

Open
blsmth wants to merge 3 commits into
mainfrom
fix/4-portless-database-url
Open

blsmth wants to merge 3 commits into
mainfrom
fix/4-portless-database-url

Conversation

@blsmth

@blsmth blsmth commented Sep 1, 2026

Copy link
Copy Markdown

Summary

  • urlparse(...).port is None for a DATABASE_URL with no explicit :port (e.g. Neon, Crunchy, Supabase, Render, PlanetScale connection strings). Both postgres/bin/parse_database_url.py and mysql/bin/parse_database_url.py printed that as the literal string PORT=None, which got written straight into ~/.pg_service.conf / ~/.my.cnf, making every client connection fail even though the port itself was reachable.
  • Default the port in both scripts: parsed.port or 5432 (postgres) and parsed.port or 3306 (mysql). One-line change per file, config file shape is unchanged.
  • postgres/bin/entrypoint.sh's wait_for_db swallowed psql's stderr, so a bad/misparsed URL just looked like a generic connectivity timeout after 30 retries. It now captures the last error and prints it once the retry budget is exhausted (loop stays quiet on intermediate attempts; retry count/sleep interval unchanged).
  • Added regression coverage in postgres/tests/tests.sh and mysql/tests/tests.sh: assert PORT resolves to 5432/3306 for a portless DATABASE_URL, and assert a real connection succeeds against a portless URL. The existing docker-compose.test.yml files always used an explicit :5432/:3306, which is exactly why this was never caught.

Closes #4.

Scope / no-op confirmation

Managed AppPack databases always include an explicit port in their DATABASE_URL, so parsed.port is never None for them and parsed.port or <default> is a strict no-op in that path. This change only changes behavior for externally-managed databases whose connection strings omit the port.

Test plan

Ran both test suites locally with Docker (make test-postgres, make test-mysql), both exit 0:

###### Testing portless DATABASE_URL defaults PORT to 5432...
✅ PORT defaulted to 5432 for a portless DATABASE_URL

###### Testing connection succeeds with a portless DATABASE_URL...
✅ psql connected successfully using a portless DATABASE_URL

###### Testing SERVER_VERSION override...
✅ pg_dump-17 used as expected with SERVER_VERSION=17

###### Testing fallback with unset SERVER_VERSION...
✅ pg_dump-14 used as expected when SERVER_VERSION was unset
...
###### Verify dump file does not exist after load...
ok
###### Testing portless DATABASE_URL defaults PORT to 3306...
✅ PORT defaulted to 3306 for a portless DATABASE_URL

###### Testing connection succeeds with a portless DATABASE_URL...
✅ mysql connected successfully using the parsed portless DATABASE_URL
...
###### Verify dump file does not exist after load...
ok

Both GitHub Actions checks (Test MySQL, Test Postgres) are green on this PR: https://github.com/apppackio/apppack-db-utils/actions/runs/33544838121, https://github.com/apppackio/apppack-db-utils/actions/runs/33544838115

Unrelated pre-existing infra issue, fixed in this PR to unblock CI: localstack/localstack:latest moved to a unified AWS image in March 2026 that requires a paid LOCALSTACK_AUTH_TOKEN to boot at all (confirmed reproducible on main prior to this branch's changes, and the last green CI run on main was 2026-01-27). postgres/docker-compose.test.yml and mysql/docker-compose.test.yml now pin the s3 service to localstack/localstack:4.14.0, the last community-edition tag that starts without a license, with a comment explaining why and noting the pin should be revisited once a supported token-free image is available.

Not run: make image / make push-image (out of scope, publishing is a separate deploy step).

urlparse().port is None when a DATABASE_URL omits the port, which
rendered as the literal string "None" in ~/.pg_service.conf and
~/.my.cnf and broke every connection. Default to 5432/3306 in each
parse_database_url.py so the generated config keeps its shape.

Also stop swallowing the real psql error in the postgres wait_for_db
retry loop -- it now surfaces the last stderr output once retries are
exhausted instead of always reporting a generic timeout, so a
misparsed URL diagnoses itself.

Add regression coverage in both tests.sh scripts asserting PORT
defaults correctly for a portless URL and that a live connection
succeeds against the default port, since the existing DATABASE_URLs
in docker-compose.test.yml always specified an explicit port and
never exercised this path.

Fixes #4
localstack/localstack:latest switched to a unified AWS image in March
2026 that requires a paid LOCALSTACK_AUTH_TOKEN to boot at all, even
for plain S3, which breaks make test-postgres/test-mysql and CI with
"Could not connect to the endpoint URL". Pin to 4.14.0, the last tag
before that migration that still starts without a license. Revisit
once the suite can target a supported, token-free community image.
blsmth added a commit to apppackio/apppack that referenced this pull request Sep 2, 2026
DBShellTaskInfo() built `mysql --database=<app-name>`, which only works
for a managed AppPack database (named after the app). An externally-
managed MySQL (e.g. PlanetScale) has whatever database name is in its
DATABASE_URL, so the connection failed.

For non-review apps, use a bare `mysql` and let the db-utils image
supply the database, the same way psql already does via
~/.pg_service.conf. Review-app behavior is untouched: review apps
cannot use external databases (the CloudFormation condition requires
IsApp), so they keep the explicit `--database=<app>-pr<N>` form
byte-for-byte.

db dump and db load are unaffected -- dump-to-s3.sh/load-from-s3.sh
already derive the database name from DATABASE_URL via $NAME.

Deploy ordering: this changes the managed-database command too (bare
`mysql` instead of `mysql --database=<app>`), which only works once
the db-utils image writes `database=$NAME` into ~/.my.cnf (in flight
as apppackio/apppack-db-utils#5). This is safe because the image is
served from a mutable tag
(public.ecr.aws/d9q4v8a4/apppack-db-utils:mysql, rebuilt from main)
with no version pinning, and db-utils ships ahead of the CLI -- so
there's no window where the CLI's new bare `mysql` command reaches a
db-utils image that doesn't yet resolve the database from
DATABASE_URL.
The generated MySQL client config only carried host/port/user, so a
bare `mysql` (no --database, no positional dbname) had no default
database -- unlike psql, which already gets dbname from ~/.pg_service.conf.
This blocks moving `apppack db shell` off a hardcoded --database=<app name>,
which breaks for externally-managed MySQL databases where the app name
and the DATABASE_URL's database name diverge.

Add database=$NAME, but scoped to the [mysql] section rather than
[client]: mysqladmin and mysqldump also read [client] and reject
`database` as an unknown option, so putting it there broke the health
check and dump/load flow. load-from-s3.sh's own DROP/CREATE cycle also
needed --no-defaults + explicit connection flags, since the app DB user
has no privileges outside of $NAME and $NAME doesn't exist for the
moment between the drop and the create.

For managed AppPack databases $NAME already equals the app name (the
same value the CLI passes via --database today), so this is a no-op
for that path.

Add a tests.sh assertion that a bare `mysql` connects to the database
named in DATABASE_URL, matching the exact behavior the CLI will depend
on.
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.

parse_database_url.py emits PORT=None for URLs without an explicit port, breaking all connections

1 participant