Skip to content

Add validate_dsn for structured connection-parameter errors - #33

Open
PingoLee wants to merge 2 commits into
JuliaDatabases:mainfrom
PingoLee:validate-dsn
Open

PingoLee wants to merge 2 commits into
JuliaDatabases:mainfrom
PingoLee:validate-dsn

Conversation

@PingoLee

@PingoLee PingoLee commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

parse_dsn reports every connection-string problem as an ArgumentError whose only content is the message, so a caller that needs to know which parameter failed has to match on the text. That breaks whenever the wording changes (#28).

This adds Postgres.validate_dsn(dsn), which returns nothing when parse_dsn(dsn) would succeed and otherwise a Postgres.ConnectionParameterError with three fields:

  • key: the parameter that failed, or nothing for an unknown or empty key, a malformed URI, and malformed UTF-8 in the connection string. It is always a parameter name Postgres.jl supports or accepts and ignores, never text taken from a value.
  • reason: :unknown_key, :invalid_value, :unsupported_ignored, or :malformed.
  • msg: exactly the message parse_dsn throws for the same input; showerror prints it.

The parsing helpers now return a ConnectionParameterError instead of throwing, and both entry points share one internal parser. parse_dsn throws ArgumentError(err.msg) when it gets one, so for input it already rejected with an ArgumentError, its messages and the first error reported when several values are invalid are unchanged. Neither path wraps the other in a try/catch, so a successful parse doesn't pay for an exception and no exception chain can carry input. Environment defaults are applied as before, so an invalid PGPORT is reported by both.

parse_dsn accepts exactly the same input as before. Two kinds of rejected input get a different error:

  • Malformed UTF-8 used to make lowercase throw Base.InvalidCharError, which carries part of the input. This happened for the connection string itself, and for a percent-decoded URI value or an environment variable used in a boolean parameter, sslmode or gssencmode. It is now :malformed (connection string contains malformed UTF-8) or :invalid_value with the usual message. For sslmode/gssencmode that is the message proposed in Provide structured errors for invalid connection parameters #28 (…; expected one of disable, prefer, require, verify-full). Only what lowercase cannot decode is rejected: surrogates and code points above U+10FFFF parse as before, and values that are not lowercased, such as passwords, are still taken as they are. Other invalid sslmode/gssencmode values are still rejected only when connecting.
  • A numeric_overflow value containing a NUL byte now gets the usual numeric_overflow must be :warn or :error message instead of Julia's Symbol name may not contain \0, which was raised before the other parameters were checked.

Adds validate_dsn and ConnectionParameterError to the public API (public on Julia 1.11+), with docstrings and a short paragraph in the manual. No runtime dependency changes.

Part of #28. The sslmode/gssencmode validation will be a separate PR. This one rejects only the malformed values above, which can never match a mode and cannot be lowercased. Their message already uses the shape proposed for that PR, so if you'd prefer different wording, it can change in both places at once.

Validation:

  • New Structured connection string errors testset: for every current message shape (keyword and URI), validate_dsn returns the expected key/reason, and its msg equals the message of the ArgumentError that parse_dsn throws for the same input. It also covers valid input, environment-derived errors, the order in which several errors are reported, malformed UTF-8 (including that surrogates still parse), and that rejected sslpassword values never appear. The existing credential-safety cases now also check validate_dsn.
  • Mutation checks: reverting the src change, dropping one error propagation in params_from_values, changing one reason, swapping the order of two value checks, or letting the DSN numeric_overflow message drift from the constructor's each makes the new tests fail. So does dropping the malformed-UTF-8 check on the connection string, on boolean values, or on gssencmode; using isvalid in place of it; or moving the sslmode/gssencmode check before port or after gssdelegation.
  • Side-by-side runs against the previous parser: about 20k generated keyword and URI DSNs, then 300k more in valid UTF-8 and 200k with injected invalid bytes. parse_dsn accepted exactly the same input with the same field values, and failed with the same message apart from the two cases above. validate_dsn never threw, returned nothing exactly when parse_dsn succeeded, and otherwise returned its message.
  • Full Pkg.test() with Docker integration (POSTGRES_REQUIRE_INTEGRATION=true, PostgreSQL 16, Linux x64): 9107 checks pass on Julia 1.13.2, 9081 on Julia 1.10.12, and 9107 with -t 2.
  • The docs build passes.
  • Fork CI on 8d1ff71 passed all 14 jobs (Linux/Windows/macOS, Julia min/1/pre, PostgreSQL 14-18, SCRAM/MD5); this PR's CI covers the follow-up commit. The branch merges cleanly with current main.

Drafted with Claude Code (Claude Opus 5.5) and reviewed by me before posting.

🤖 Generated with Claude Code

parse_dsn reports every connection-string problem as an ArgumentError whose
only content is the message, so a caller that needs to know which parameter
failed has to match on text that can change between releases (JuliaDatabases#28).

Add Postgres.validate_dsn, which returns nothing or a
Postgres.ConnectionParameterError carrying the parameter name (never text
from a value), a reason symbol, and the message parse_dsn throws. The
parsing helpers now return the error instead of throwing, and both entry
points share one parser; parse_dsn throws ArgumentError(err.msg), so its
exception type, messages and error order are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@quinnj-bot quinnj-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[comment generated via AI]

I fuzzed this with 200k generated keyword and URI connection strings, including quoting, escapes, % sequences, NUL bytes and random bytes. validate_dsn and parse_dsn agreed on every valid UTF-8 input: nothing exactly when parse_dsn succeeds, and otherwise the same message.

One gap: a string that isn't valid UTF-8 makes validate_dsn throw Base.InvalidCharError, for example Postgres.validate_dsn("host=h\xff"). lowercase(dsn) in try_parse_dsn throws it, and parse_dsn on main throws the same error. Since validate_dsn promises nothing or a ConnectionParameterError, an up-front isvalid(dsn) check returning a :malformed error would close the gap. parse_dsn would then throw an ArgumentError for such strings, like the other syntax errors.

A connection string with malformed UTF-8 made both validate_dsn and
parse_dsn throw Base.InvalidCharError from lowercase. So did a
percent-decoded URI value or an environment variable with such bytes in a
boolean parameter, sslmode or gssencmode. validate_dsn is documented to
return nothing or a ConnectionParameterError.

Report a connection string with a malformed or overlong character as
:malformed, and such a boolean, sslmode or gssencmode value as
:invalid_value. Only what lowercase cannot decode is rejected: surrogates
and code points above U+10FFFF parse as before, and values that are not
lowercased, such as a percent-decoded password, are still taken as they
are.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@PingoLee

PingoLee commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks for fuzzing this. Confirmed, and the gap was a little wider: a valid UTF-8 URI can percent-decode to malformed bytes (?reconnect=%ff), and an environment variable can contain them. Both reached lowercase in the boolean parser and for sslmode/gssencmode, so the up-front check alone would not have closed it.

54fa9cf rejects exactly what lowercase cannot decode (a malformed or overlong character), rather than everything isvalid rejects. Surrogates and code points above U+10FFFF parse today, so a password containing one would otherwise stop working.

  • A connection string with such a character: :malformed, connection string contains malformed UTF-8.
  • Such a boolean value: :invalid_value, with the usual message.
  • Such an sslmode/gssencmode value: :invalid_value at parse time, with the message proposed in Provide structured errors for invalid connection parameters #28. Other invalid values are still checked when connecting; the separate PR will cover them.

parse_dsn accepts the same input as before. These cases now raise an ArgumentError instead of InvalidCharError. I've updated the PR description to match.

Drafted with Claude Code (Claude Opus 5.5) and reviewed by me before posting.

This branch has not been deployed

No deployments
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.

1 participant