Summary
buildDSNFromSource() builds a SQLite DSN by unconditionally prefixing three slashes:
https://github.com/bytebase/dbhub/blob/main/src/config/toml-loader.ts#L920
return `sqlite:///${source.database}`;
SQLiteDSNParser.parse() then interprets that DSN positionally, so the round-trip only preserves the configured path for Windows drive-letter paths and :memory:. Every other form is silently rewritten — POSIX absolute paths become relative, and relative paths become absolute.
Because the rewrite is silent, a source can connect successfully to the wrong file rather than failing.
Verified behaviour
dbhub 1.2.1, Node v24.19.0. dbPath is the value handed to node:sqlite:
TOML database |
DSN built |
Path actually opened |
|
C:/x/y.db |
sqlite:///C:/x/y.db |
C:/x/y.db |
correct |
/var/lib/y.db |
sqlite:////var/lib/y.db |
var/lib/y.db |
absolute → relative |
data/y.db |
sqlite:///data/y.db |
/data/y.db |
relative → absolute |
./data/y.db |
sqlite:///./data/y.db |
/data/y.db |
relative → absolute |
:memory: |
sqlite:///:memory: |
:memory: |
correct |
Reproduction
The relative case fails loudly:
[[sources]]
id = "app"
type = "sqlite"
database = "data/app.db" # file exists at <cwd>/data/app.db
- app: sqlite:///data/app.db
Failed to connect to SQLite database: Error: unable to open database file
code: 'ERR_SQLITE_ERROR', errcode: 14
The absolute case fails silently, which is the more serious half. With a database at <cwd>/sub/a.db and nothing at the filesystem root:
[[sources]]
id = "abs"
type = "sqlite"
database = "/sub/a.db" # absolute path
The server starts and queries succeed — against <cwd>/sub/a.db. The configured absolute path was reinterpreted as relative, and the connection went to a different file than the config names.
Root cause
sqlite:///${database} collides with the positional scheme that SQLiteDSNParser documents and implements:
sqlite:///path → pathname /path → falls through to dbPath = url.pathname (absolute)
sqlite://./path → relative
sqlite:///C:/path → matched by the ^\/[A-Za-z]:\// branch and de-prefixed
So a relative database lands in the absolute branch, and a POSIX absolute database produces four slashes, hits pathname.startsWith("//"), and gets substring(2) — dropping the leading slash.
Why CI doesn't catch it
Every SQLite source in the TOML fixtures uses database = ":memory:" — one of the two forms that round-trips correctly:
src/__fixtures__/toml/multi-sqlite.toml
src/__fixtures__/toml/readonly-maxrows.toml
Suggested fix
Resolve database in buildDSNFromSource before constructing the DSN:
- Pass
:memory: through unchanged.
- Resolve a relative
database against the directory of the config file, not process.cwd() — a config should mean the same thing regardless of where the server is launched from. This needs configPath threaded into buildDSNFromSource.
- Encode the resolved absolute path so the parser's branches recover it exactly on both POSIX and Windows.
Happy to open a PR — I have the fix and fixture coverage for the matrix above. Let me know if you'd prefer relative paths resolved against the config file or rejected with a clear error; I've assumed the former.
Summary
buildDSNFromSource()builds a SQLite DSN by unconditionally prefixing three slashes:https://github.com/bytebase/dbhub/blob/main/src/config/toml-loader.ts#L920
SQLiteDSNParser.parse()then interprets that DSN positionally, so the round-trip only preserves the configured path for Windows drive-letter paths and:memory:. Every other form is silently rewritten — POSIX absolute paths become relative, and relative paths become absolute.Because the rewrite is silent, a source can connect successfully to the wrong file rather than failing.
Verified behaviour
dbhub 1.2.1, Node v24.19.0.
dbPathis the value handed tonode:sqlite:databaseC:/x/y.dbsqlite:///C:/x/y.dbC:/x/y.db/var/lib/y.dbsqlite:////var/lib/y.dbvar/lib/y.dbdata/y.dbsqlite:///data/y.db/data/y.db./data/y.dbsqlite:///./data/y.db/data/y.db:memory:sqlite:///:memory::memory:Reproduction
The relative case fails loudly:
The absolute case fails silently, which is the more serious half. With a database at
<cwd>/sub/a.dband nothing at the filesystem root:The server starts and queries succeed — against
<cwd>/sub/a.db. The configured absolute path was reinterpreted as relative, and the connection went to a different file than the config names.Root cause
sqlite:///${database}collides with the positional scheme thatSQLiteDSNParserdocuments and implements:sqlite:///path→ pathname/path→ falls through todbPath = url.pathname(absolute)sqlite://./path→ relativesqlite:///C:/path→ matched by the^\/[A-Za-z]:\//branch and de-prefixedSo a relative
databaselands in the absolute branch, and a POSIX absolutedatabaseproduces four slashes, hitspathname.startsWith("//"), and getssubstring(2)— dropping the leading slash.Why CI doesn't catch it
Every SQLite source in the TOML fixtures uses
database = ":memory:"— one of the two forms that round-trips correctly:src/__fixtures__/toml/multi-sqlite.tomlsrc/__fixtures__/toml/readonly-maxrows.tomlSuggested fix
Resolve
databaseinbuildDSNFromSourcebefore constructing the DSN::memory:through unchanged.databaseagainst the directory of the config file, notprocess.cwd()— a config should mean the same thing regardless of where the server is launched from. This needsconfigPaththreaded intobuildDSNFromSource.Happy to open a PR — I have the fix and fixture coverage for the matrix above. Let me know if you'd prefer relative paths resolved against the config file or rejected with a clear error; I've assumed the former.