Skip to content

CXH-2380: reach the DB2 native DSN form through connector config - #149

Open
al-conductorone wants to merge 3 commits into
mainfrom
cxh-2380-baton-sql-fix-the-db2-native-dsn-form-being-unreachable
Open

CXH-2380: reach the DB2 native DSN form through connector config#149
al-conductorone wants to merge 3 commits into
mainfrom
cxh-2380-baton-sql-fix-the-db2-native-dsn-form-being-unreachable

Conversation

@al-conductorone

Copy link
Copy Markdown
Contributor

The Db2 native connection-string form documented for this connector now works through configuration, so a customer following the docs can connect instead of hitting a confusing setup error.

A native DB2 DSN (opaque ODBC keyword string like HOSTNAME=...;DATABASE=...)
was documented in docs/db2.md but unreachable through config: the engine forced
every DSN through url.Parse/String, so a scheme-less native form hit "database
scheme must be specified" and a native form with scheme:db2 got mangled into
db2://HOSTNAME=... and hit "database name is required in DSN path".

Connect now detects a native DB2 DSN and hands it to the driver verbatim,
bypassing the URL builder. ResolveDatabaseName extracts DATABASE= so the
resolved database name matches the equivalent db2://.../DB URL, keeping
resource IDs stable across the two DSN forms.
@linear-code

linear-code Bot commented Sep 2, 2026

Copy link
Copy Markdown

CXH-2380

Comment thread pkg/database/database.go
Comment on lines +446 to +456
nativeDSN, isNativeDB2, err := nativeDB2DSN(opts)
if err != nil {
return nil, Unknown, err
}
if isNativeDB2 {
db, err := db2.Connect(ctx, nativeDSN)
if err != nil {
return nil, Unknown, err
}
return db, DB2, nil
}

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.

🟠 Bug: the native path hands the DSN to the driver verbatim and so ignores opts.Database. ConnectMany relies on perOpts.Database = name to open one handle per database (database.go:423-425), so a databases.static / discovery_query config plus a native DB2 DSN opens N handles that all point at the DSN's DATABASE=, keyed by the different names — every "database" then syncs identical rows under a distinct _database label, producing duplicated/mislabeled resources instead of an error. The single-database case has the mirror problem: ResolveDatabaseName returns opts.Database first (line 359), so connect.database: FOO with a native DSN keys the handle FOO while the connection is to the DSN's database. Either rewrite the DATABASE= keyword in the DSN when opts.Database is set, or reject the native-DSN + database/databases combination with an explicit error.

Comment thread pkg/database/database.go
if !strings.Contains(dsn, "HOSTNAME=") && !strings.Contains(dsn, "DATABASE=") {
return "", false, nil
}
return dsn, true, nil

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.

🟡 Suggestion: once this returns true, the remaining structured connect fields (Host, Port, User, Password, Params) are silently dropped, since the native path never reaches buildConnectionURL. A config mixing a native DSN with e.g. connect.params will connect without those settings and give no signal. Consider returning an error when any of those fields are non-empty alongside a native DSN, and noting the exclusivity in docs/db2.md.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Connector PR Review: CXH-2380: reach the DB2 native DSN form through connector config

Blocking Issues: 0 | Suggestions: 3 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base 2963cce98f5b.
Review mode: full
View review run

Review Summary

Scanned the full PR diff for security and correctness: native DB2 DSN routing in Connect / ResolveDatabaseName, the convertToDB2DSN passthrough, the CI action bumps, the docs change, and the three test files (go.mod / go.sum unchanged; testify is already a direct dependency). Both prior findings are addressed — marker detection and DATABASE= extraction are now case-insensitive and whitespace-tolerant via EqualFold plus TrimSpace (pkg/database/database.go:379-404, pkg/database/db2/dsn.go:19), and account-provisioning is bumped to @v4 so it no longer depends on the earlier sync-test step having installed the pebble-capable CLI (both v4 tags exist in ConductorOne/github-workflows). No blocking issues; three non-blocking suggestions on brace parsing, duplicated detection logic, and one documentation claim.

Security Issues

None found. The new rejection error does not echo the DSN, and no credential material is added to logs, errors, or spans.

Correctness Issues

None found. Native-DSN routing only captures scheme-less DSNs that carry a HOSTNAME= or DATABASE= keyword with an empty or db2 scheme, and every such config previously failed inside the URL builder — so there is no behavior regression for the db2:// URL form or for the other engines, and the new mutual-exclusivity error only fires on combinations that never worked.

Suggestions

  • pkg/database/database.go:417-435splitDB2DSN enters ODBC brace-quoting on any brace character rather than only on a value that begins with one, so a value such as PWD=p{q swallows the rest of the DSN into a single part. db2DSNDatabase then returns an empty string while the driver still connects to the real database, producing an empty handle key and a dropped Database row column (pkg/bsql/query.go:819).
  • pkg/database/db2/dsn.go:19-30isNativeDB2Format duplicates database.hasDB2Marker and urlSchemeDSNRegex, and the two copies already disagree (plain semicolon split vs. the brace-aware splitDB2DSN), which makes the markers-match claim in the nativeDB2DSN doc comment inaccurate. Exporting one detector from the db2 package would keep routing and passthrough from drifting.
  • docs/db2.md:113-114 — the documented whitespace tolerance is implemented only in baton-sql's own parsing; the native DSN reaches go_ibm_db verbatim with the spaces intact, so worth confirming the driver accepts a leading space before a keyword (or trimming each part before handoff) before documenting it as supported. Low confidence — depends on driver behavior.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Suggestions

In `pkg/database/database.go`:
- Around line 417-435: splitDB2DSN treats every open-brace character as starting ODBC
  brace quoting and every close-brace as ending it. ODBC only quotes when a value
  *starts* with an open brace, and the quoted value ends at the matching close brace.
  As written, a DSN of the form HOSTNAME=h;PWD=p<open-brace>q;DATABASE=TESTDB splits
  into only two parts, so db2DSNDatabase returns an empty string even though the driver
  connects to TESTDB (ODBC reads that password literally). The sql.DB map key then
  becomes empty, pkg/bsql/query.go:819 omits the Database row column, and resource IDs
  diverge from the equivalent db2:// URL. Fix by entering brace mode only when the
  character immediately after the first = of a part is an open brace, and leaving brace
  mode at the first close brace after that.

In `pkg/database/db2/dsn.go`:
- Around line 19-30: isNativeDB2Format duplicates hasDB2Marker and urlSchemeDSNRegex
  from pkg/database/database.go, and the two already differ: this one splits with
  strings.Split on the semicolon while hasDB2Marker uses the brace-aware splitDB2DSN.
  That makes the nativeDB2DSN doc comment claiming the markers match convertToDB2DSN's
  passthrough wrong for a DSN whose only marker sits inside a brace-quoted value.
  Export a single detector from the db2 package (for example db2.IsNativeDSN), have
  pkg/database call it, and delete the duplicated regex and marker loop from
  database.go so the routing decision and the passthrough decision cannot drift.

In `docs/db2.md`:
- Around line 113-114: the note that ODBC keywords may carry spaces after each
  semicolon holds for baton-sql's own detection and extraction, which trim each part,
  but the native DSN itself is handed to go_ibm_db verbatim with the spaces intact, so
  it only works if the CLI driver tolerates a leading space before the keyword. Either
  verify a spaced native DSN connects against a real DB2 instance, or normalize the DSN
  by trimming each semicolon-separated part before passing it to db2.Connect, and keep
  the documentation aligned with whichever is true.

Reviewed head 640c973c8e6edbd91ecfb507c6b648cdb4b1c20f against base 2963cce98f5b53d2eee77a3323ad55c3f46af843. The machine-readable review-state marker could not be written this run: the sandbox rejects the JSON-in-command pattern needed to emit it, so the next review will run in full mode rather than incrementally.

@github-actions github-actions 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.

Blocking issues found — see review comments.

sync-test@v2 installs the CLI from conductorone/baton (latest v0.4.5,
pre-pebble), so baton grants rejects the pebble-format c1z with
"c1z: invalid file". @v4 pulls the CLI from conductorone/baton-sdk,
which reads pebble.
Comment thread pkg/database/database.go Outdated
Comment on lines +545 to +550
if strings.Contains(dsn, "://") {
return "", false, nil
}
if !strings.Contains(dsn, "HOSTNAME=") && !strings.Contains(dsn, "DATABASE=") {
return "", false, nil
}

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.

🟡 Suggestion: native-DSN detection and DATABASE= extraction are both byte-exact/uppercase-only, but ODBC keywords are case-insensitive and ; spacing is common, so two valid native DSNs still misbehave. hostname=h;database=X matches neither marker, falls through to the URL path, and hits the exact database scheme must be specified error this PR set out to fix. Worse, HOSTNAME=h; DATABASE=X (or HOSTNAME=h;database=X) is detected and connects, but db2DSNDatabase (line 374) fails its CutPrefix("DATABASE=") and returns ""openDatabases then keys the handle with "" (connector.go:191), query.go:819 skips injecting the Database row column, and resource IDs no longer match the equivalent db2://.../X URL, defeating the stated ID-stability goal. Consider matching keywords case-insensitively and trimming whitespace around each ;-separated part in both nativeDB2DSN and db2DSNDatabase.

Comment thread .github/workflows/ci.yaml
run: go build ./cmd/baton-sql
- name: Run sync tests
uses: ConductorOne/github-workflows/actions/sync-test@v2
uses: ConductorOne/github-workflows/actions/sync-test@v4

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.

🟡 Suggestion: account-provisioning@v3 (line 51) still resolves get-baton@v2, which installs the CLI from conductorone/baton (pre-pebble) — the same incompatibility this bump fixes. It passes today only because get-baton.sh early-exits when baton is already on PATH, so this sync-test@v4 step has already installed the baton-sdk CLI. Reordering, skipping, or removing this step would silently reintroduce c1z: invalid file in the provisioning step; bumping account-provisioning to @v4 (which pins get-baton@v4) removes the ordering dependency.

@github-actions github-actions 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.

No new blocking issues in this pass, but the two blocking findings from the previous review are still unaddressed at 209040e (pkg/database/database.go:446 ignoring opts.Database on the native path, and pkg/database/database.go:551 silently discarding structured connect fields). Two new suggestions posted inline.

Address the CI review findings on the native DB2 DSN path:

- Reject a native DSN combined with structured connect fields (host, port,
  user, password, params) or a per-database override (connect.database,
  databases). The verbatim path never reaches buildConnectionURL, so those
  were silently dropped and multi-database sync opened every handle against
  the DSN's single DATABASE=. Now it errors clearly instead.
- Detect native DSNs and extract DATABASE= case-insensitively and with
  whitespace tolerance (ODBC keywords are case-insensitive; "; " spacing is
  common). A lowercase/spaced native DSN previously fell through to the URL
  path and hit "scheme must be specified", or resolved an empty database name.
- Apply the same case-insensitive passthrough in db2.convertToDB2DSN, which
  the native path now reaches for lowercase DSNs.
- Anchor the URL-shape check to a leading scheme so a native DSN whose value
  contains "://" (e.g. PWD=my://secret) is not misread as a URL.
- Bump account-provisioning to @v4 so it no longer depends on step ordering
  to pick up the pebble-compatible CLI.
- Document native-DSN exclusivity and case-insensitivity in docs/db2.md.

Verified live against a local DB2 container: lowercase native DSN syncs,
native DSN + connect.database is rejected, uppercase and db2:// URL forms
unaffected.
Comment thread pkg/database/database.go
Comment on lines +417 to +435
func splitDB2DSN(dsn string) []string {
var parts []string
start := 0
inBraces := false
for i, r := range dsn {
switch r {
case '{':
inBraces = true
case '}':
inBraces = false
case ';':
if !inBraces {
parts = append(parts, dsn[start:i])
start = i + 1
}
}
}
return append(parts, dsn[start:])
}

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.

🟡 Suggestion: brace tracking here toggles on any {/}, but ODBC only treats braces as quoting when a value starts with { (ending at the matching }). So HOSTNAME=h;PWD=p{q;DATABASE=TESTDB collapses into two parts (HOSTNAME=h, PWD=p{q;DATABASE=TESTDB): the driver still connects to TESTDB (it reads p{q literally), but db2DSNDatabase returns "", so the handle key is empty, rowColDatabase is dropped (pkg/bsql/query.go:819) and resource IDs diverge from the equivalent db2:// URL. Consider entering brace mode only when the char after = is {, and leaving it at the first }. (confidence: high on behavior, narrow trigger)

Comment thread pkg/database/db2/dsn.go
Comment on lines +19 to +30
func isNativeDB2Format(dsn string) bool {
if urlSchemeRegex.MatchString(dsn) {
return false
}
for _, part := range strings.Split(dsn, ";") {
keyword, _, found := strings.Cut(strings.TrimSpace(part), "=")
if found && (strings.EqualFold(keyword, "HOSTNAME") || strings.EqualFold(keyword, "DATABASE")) {
return true
}
}
return false
}

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.

🟡 Suggestion: this duplicates database.hasDB2Marker + urlSchemeDSNRegex, and the two copies already disagree — this one splits with plain strings.Split(dsn, ";") while hasDB2Marker uses the brace-aware splitDB2DSN. So UID=u;PWD={x;DATABASE=y} is "native" here but not to nativeDB2DSN, which makes the nativeDB2DSN doc claim "Markers match db2.convertToDB2DSN's passthrough" inaccurate. Consider exporting one detector from this package (e.g. db2.IsNativeDSN) and having pkg/database call it, so the routing decision and the passthrough decision cannot drift.

Comment thread docs/db2.md
Comment on lines +113 to +114
DB2's native form is also accepted as-is (ODBC keywords are case-insensitive and may carry
spaces after each `;`):

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.

🟡 Suggestion: the case-insensitivity claim is backed by the new EqualFold detection, but the whitespace claim only covers baton-sql's own parsing — the native DSN is handed to go_ibm_db/CLI verbatim, spaces included, so HOSTNAME=h; DATABASE=X connects only if the driver tolerates the leading space in the keyword. The commit notes a lowercase DSN was verified live but not a spaced one; worth confirming against the driver (or trimming each part before passthrough) before documenting it as supported. (confidence: low — depends on driver behavior)

@github-actions github-actions 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.

No blocking issues found.

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