Skip to content

SQLite TOML database path is rewritten: POSIX absolute becomes relative, relative becomes absolute #411

Description

@darylmcd

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:

  1. Pass :memory: through unchanged.
  2. 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.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions