Skip to content

fix: TOML config loading, DSN conflict, and .env interpolation#368

Merged
tianzhou merged 8 commits into
mainfrom
fix/dsn-toml-conflict
Jul 19, 2026
Merged

fix: TOML config loading, DSN conflict, and .env interpolation#368
tianzhou merged 8 commits into
mainfrom
fix/dsn-toml-conflict

Conversation

@tianzhou

@tianzhou tianzhou commented Jul 19, 2026

Copy link
Copy Markdown
Member

Three related fixes to how database sources are configured. Two are breaking. Follow-up to #367, where investigating a test failure surfaced the first.

1. TOML config is loaded only via --config

resolveTomlConfigPath() fell back to ./dbhub.toml in the current working directory when --config was absent. Since TOML config selects the database outright, that meant running DBHub from the wrong directory could silently repoint it at a different database — or make an explicitly supplied DSN look ignored for no reason:

$ npx tsx src/index.ts --dsn="sqlite://:memory:"     # in a dir with dbhub.toml
Configuration source: dbhub.toml
Fatal error: error: database "employee" does not exist

The user asked for SQLite and got a Postgres error, never having mentioned TOML.

Nothing shipped depends on the old fallback: the MCPB bundle already passes --config ${__dirname}/dbhub.toml (mcpb/manifest.json), and every documented invocation passes --config explicitly.

2. --config and --dsn cannot be combined

resolveSourceConfigs() returned the TOML config before resolveDSN() was ever consulted, so --dsn was silently ignored. The documented priority order said the opposite — CLAUDE.md and docs/config/command-line.mdx both claimed "command-line arguments (highest priority)" over TOML.

TOML wins, and the docs were wrong. The two are conflicting rather than layered: TOML is multi-database, a DSN is single-database. Passing both flags now errors, mirroring the existing --id guard:

Error: The --dsn flag cannot be used with TOML configuration (dbhub.toml). ...

The guard is deliberately limited to the flag. DSN and DB_* environment variables are not conflicts, whether exported or read from .env — see below.

3. .env is loaded before TOML, so ${VAR} interpolation resolves

interpolateEnvVars() reads process.env, but .env was only loaded inside resolveDSN() — the fallback path that never runs when --config is given. So the recommended pattern of keeping credentials out of the config file did not work:

# .env
DSN=postgres://user:pass@localhost:5432/mydb
# dbhub.toml
[[sources]]
id = "prod"
dsn = "${DSN}"
Configuration source: dbhub.toml
  - prod: ${DSN}
Fatal error: No connector found for DSN: ${DSN}

This is the same fix as #358 by @kevinke, narrowed in scope and with a regression test — that PR can be closed once this merges.

The narrowing matters: #358 calls loadEnvFiles() unconditionally. On the DSN path resolveDSN() already loads .env itself, and it does so after checking process.env so it can report whether a value came from the environment or from the file. Preloading unconditionally relabels every .env-sourced DSN as environment variable in the startup log. Scoping the load to TOML mode avoids that; verified the DSN path still reports Configuration source: .env file.

Why fixes 2 and 3 belong together

They pull in opposite directions, and getting one right requires the other. An earlier revision of this PR rejected any DSN-shaped environment variable as a conflict — which would have broken the interpolation pattern above outright. Only --dsn expresses unambiguous intent; environment variables are config material for TOML, not a competing setup.

⚠️ Breaking changes

  1. dbhub.toml in the current directory is no longer loaded. Pass --config ./dbhub.toml.
  2. --config together with --dsn now fails to start instead of silently using TOML.

Both warrant a release note. The second was chosen over a warning because silent-ignore is what produced the confusing failure above, and a stderr warning is easy to miss in stdio mode.

Docs

Both docs described a single priority ladder. The accurate model is two independent rules, since TOML only defines sources and tools and cannot express transport/port/host:

  • Source selection: TOML (via --config) or a DSN. Within DSN mode: --dsn > DSN > DB_* > .env.
  • Everything else: CLI > env > .env > defaults (unchanged, always correct).

Rewritten to describe the model directly rather than contrast against previous behavior. Updated CLAUDE.md, docs/config/command-line.mdx, docs/config/toml.mdx.

Verification

  • 1107 tests passing. New coverage: --dsn + --config rejected; DSN env var + --config allowed; DB_* + --config allowed; .env loaded before TOML parsing; a cwd dbhub.toml ignored.
  • The .env-before-TOML test was confirmed to fail without the fix, not merely pass alongside it.
  • The 106 toml-loader tests that wrote dbhub.toml into a temp cwd now point --config at it via one line in beforeEach, rather than 106 edits.
  • Manually verified against real configs: dsn = "${DSN}" resolves from both an exported variable and a .env file; the DSN path still reports .env file as its source; --dsn + --config errors; cwd dbhub.toml ignored; missing --config path still errors.
  • Only failure is sqlserver.integration.test.ts (container won't boot on arm64) — fails identically on clean main.

Rebased onto main after #367 merged.

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings July 19, 2026 15:03

Copilot AI 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.

Pull request overview

This PR makes TOML-based multi-database configuration mutually exclusive with single-database DSN configuration, rejecting ambiguous setups (e.g., dbhub.toml present alongside --dsn, DSN, DB_*, or DSN in .env) with an explicit, actionable error. It also updates the documentation to reflect the clarified configuration model and adds tests around DSN conflict detection and side-effect-free .env inspection.

Changes:

  • Split env file discovery into findEnvFile() so DSN conflict detection can inspect .env contents without mutating process.env.
  • Add detectDSNConfig() and enforce TOML/DSN mutual exclusivity in resolveSourceConfigs(), reporting the DSN’s origin in the error.
  • Update docs (and CLAUDE.md) to describe the two-rule model: “sources” are TOML-or-DSN, while all other settings follow the normal CLI/env precedence; add tests for the new detection logic.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/config/env.ts Adds side-effect-free DSN detection and rejects TOML+DSN combinations; refactors env file discovery.
src/config/tests/env.test.ts Adds tests for DSN detection and TOML/DSN conflict handling.
docs/config/toml.mdx Documents TOML/DSN mutual exclusivity and the startup error behavior.
docs/config/command-line.mdx Rewrites the priority explanation into separate “database sources” vs “other settings” rules and documents the exclusivity constraint.
CLAUDE.md Updates configuration guidance and priority rules to match the new behavior.

Comment thread src/config/env.ts Outdated
Comment thread src/config/env.ts Outdated
Comment thread src/config/__tests__/env.test.ts Outdated
Comment thread src/config/__tests__/env.test.ts
@tianzhou tianzhou changed the title fix: reject DSN configuration combined with TOML config fix: make TOML config explicit-only and reject DSN combined with it Jul 19, 2026
tianzhou and others added 4 commits July 19, 2026 23:34
TOML config and a DSN express conflicting intents: TOML defines sources
for one or more databases, a DSN configures exactly one. Previously
resolveSourceConfigs() returned the TOML config before resolveDSN() was
ever consulted, so a DSN — including an explicit --dsn flag — was
silently ignored with no indication of why.

Now the combination is rejected at startup with an error naming the
conflicting DSN source, mirroring the existing --id guard.

Detection is side-effect free. It deliberately does not call resolveDSN(),
which loads .env files into process.env: in TOML mode .env is otherwise
never loaded, and loading it would silently start applying unrelated
settings such as PORT and TRANSPORT. Instead findEnvFile() is split out of
loadEnvFiles() so a .env file can be inspected with dotenv.parse without
being applied.

Also corrects the documented priority order, which claimed command-line
arguments outrank TOML. That was never true for source configuration.
CLAUDE.md and the docs now separate source selection (TOML or DSN,
mutually exclusive) from every other setting (CLI > env > .env >
defaults), which TOML cannot express.

BREAKING CHANGE: a setup with both a dbhub.toml and any DSN configuration
now fails to start instead of silently using the TOML config.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
resolveTomlConfigPath() fell back to ./dbhub.toml in the current working
directory when --config was absent. Because TOML config selects the
database outright and cannot be combined with a DSN, that implicit
discovery meant running DBHub from the wrong directory could silently
repoint it at a different database, or make an explicitly supplied DSN
appear to be ignored for no visible reason.

TOML is now loaded only when --config names it. This also shrinks the
blast radius of the DSN/TOML conflict error added in the previous commit:
the conflict can now only arise from an explicit --config flag, never
from a stray file in the working directory.

The MCPB bundle already passes --config ${__dirname}/dbhub.toml
explicitly, and every documented invocation does the same, so no shipped
configuration depends on auto-discovery.

BREAKING CHANGE: a dbhub.toml in the current directory is no longer
loaded automatically. Pass --config ./dbhub.toml to load it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rewrite the configuration docs to state how DBHub picks a database rather
than contrasting against earlier behavior, and trim the same framing from
the surrounding code comments.

Also restores resolveDSN's doc comment, which had been separated from its
function by the detectDSNConfig insertion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
TOML `${VAR}` interpolation reads process.env, so `dsn = "${DSN}"` is a
supported way to keep credentials out of the config file. Treating a DSN
environment variable — exported or read from .env — as a conflict would
break that pattern, which is the whole point of interpolation.

Only the --dsn flag is unambiguous intent, so only that throws. This also
drops the .env inspection the broader check needed, along with the
findEnvFile export added for it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@tianzhou
tianzhou force-pushed the fix/dsn-toml-conflict branch from 41c8d59 to 880fdc9 Compare July 19, 2026 15:35
interpolateEnvVars() reads process.env, but .env was only loaded inside
resolveDSN() — the fallback path that never runs when --config is given.
So the recommended pattern of keeping credentials in .env and referencing
them from the config file did not work:

  # .env
  DSN=postgres://user:pass@localhost:5432/mydb

  # dbhub.toml
  dsn = "${DSN}"

resolved to the literal "${DSN}" and failed with "No connector found".

Loading is scoped to TOML mode rather than unconditional. On the DSN path
resolveDSN() loads .env itself, and it has to do so after checking
process.env so it can report whether a value came from the environment or
from the file; preloading would relabel every .env-sourced DSN as an
environment variable.

Same fix as #358 by @kevinke, narrowed in scope and with a regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@tianzhou tianzhou changed the title fix: make TOML config explicit-only and reject DSN combined with it fix: TOML config loading, DSN conflict, and .env interpolation Jul 19, 2026
@tianzhou
tianzhou requested a review from Copilot July 19, 2026 15:45
It pointed at an isReadOnlyMode() function that does not exist, and
described a deprecation warning where parseCommandLineArgs() actually
exits on --readonly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI 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.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Comment thread src/config/toml-loader.ts Outdated
Comment thread src/config/env.ts
Comment thread docs/config/command-line.mdx Outdated
--config now goes through requireFlagValue(), so a bare `--config` or
`--config=` reports "--config requires a value" instead of resolving to
the sentinel "true" and failing as `not found: true`. That helper already
existed for --host and --allowed-hosts and documents itself as reusable.
This matters more now that --config is the only way to load TOML.

findEnvFile() also checked the working directory twice: a bare relative
filename resolves against process.cwd(), so it was identical to the
explicit path.join(process.cwd(), ...) entry. Removed the trailing
duplicate rather than the leading one, which would have flipped
precedence so the package root won over the working directory.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI 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.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Comment thread src/config/__tests__/env.test.ts
Comment thread src/config/__tests__/env.test.ts
The tests mocked loadTomlConfig() into returning a config while argv had
no --config. Since --config is the only way TOML loads, that state cannot
occur at runtime — and it specifically skipped the .env-before-TOML load,
which is gated on the flag, so those tests exercised a path production
never takes.

They now pass --config, and run from an empty temp directory so an
ambient .env cannot reach the load the flag now triggers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI 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.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Comment thread src/config/env.ts
Comment on lines +116 to +120
if (envPath) {
dotenv.config({ path: envPath });

// Check for deprecated environment variables
if (process.env.READONLY !== undefined) {
@tianzhou
tianzhou merged commit 871aa39 into main Jul 19, 2026
4 checks passed
@tianzhou
tianzhou deleted the fix/dsn-toml-conflict branch July 19, 2026 16:49
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.

2 participants