Skip to content

[BB-1827] address expand dsn edge cases - #97

Merged
ggreer merged 2 commits into
mainfrom
bugfix/address_expand-dsn_edge_cases
Dec 2, 2025
Merged

[BB-1827] address expand dsn edge cases#97
ggreer merged 2 commits into
mainfrom
bugfix/address_expand-dsn_edge_cases

Conversation

@agustin-conductor

@agustin-conductor agustin-conductor commented Dec 2, 2025

Copy link
Copy Markdown
Contributor

Description

  • [X ] Bug fix
  • New feature

Useful links:

Summary by CodeRabbit

  • Bug Fixes

    • Improved environment variable expansion in database connection strings with collision-resistant sentinels
    • More resilient handling when env vars are missing and better edge-case substitution (including ports and embedded host paths)
    • Enhanced parsing to ensure numeric ports and correct path placement in DSNs
  • Tests

    • Updated test coverage for placeholder and sentinel expansion scenarios

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Dec 2, 2025

Copy link
Copy Markdown

Walkthrough

Refactors DSN environment-variable expansion to use collision-avoidant string sentinels (_PH-<number>_), adds pre-parse port-sentinel replacement, handles host values with embedded paths, and updates expansion sequencing to expand scheme, port, and per-component values safely.

Changes

Cohort / File(s) Change Summary
DSN Expansion Logic
pkg/database/database.go
Replaces numeric placeholders with _PH-<number>_ sentinels; defers env lookup to mapping population; adds expandPortSentinel to replace port sentinels before URL parsing; handles host values containing embedded path components and prepends extracted path to DSN path; updates expandDSN to handle full-ENV-DSN, expand scheme sentinels pre-parse, and perform per-component expansion (userinfo, host, path, query, fragment).
DSN Expansion Tests
pkg/database/database_test.go
Adjusts placeholder sentinel expectations to new _PH-<number>_ format; adds test verifying port replacement in DSN; updates assertions for single, multiple, and repeated placeholder scenarios.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Verify sentinel encoding and mapping population (no collisions, correct removal of consumed sentinels).
  • Inspect expandPortSentinel for correct detection/removal and numeric-port preservation before URL parsing.
  • Review host/path splitting logic to ensure paths are safely prepended and edge cases covered.
  • Confirm expandDSN sequencing (scheme pre-parse, port pre-parse, then URL parse and per-component expansion).
  • Validate updated/added tests align with behavior and cover edge cases.

Possibly related PRs

Suggested reviewers

  • btipling
  • laurenleach

Poem

🐇 I hopped through DSNs with careful art,
Replacing sentinels, each tiny part.
Ports revealed before the parse began,
Paths unwrapped like carrots in my hand—
A tidy patch from this rabbit's heart.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: addressing edge cases in DSN expansion, which aligns with the substantial refactoring of expandDSN and related functions documented in the summary.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch bugfix/address_expand-dsn_edge_cases

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
pkg/database/database.go (1)

275-286: Redundant environment variable lookup.

The entire-DSN special case performs a second os.LookupEnv call after extractPlaceholders already looked up the variable and stored the error. Since extractPlaceholders returns an error when the variable is missing, this code path would never be reached for missing variables.

Consider simplifying by using the already-extracted mapping:

 	// Special case: if the entire DSN is a single variable (e.g., "${DSN}"),
 	// just return the expanded value directly without parsing
 	matches := DSNREnvRegex.FindAllString(dsn, -1)
 	if len(matches) == 1 && strings.TrimSpace(dsn) == strings.TrimSpace(matches[0]) {
-		// Get the variable name
-		varName := matches[0][2 : len(matches[0])-1]
-		value, exists := os.LookupEnv(varName)
-		if !exists {
-			return "", fmt.Errorf("environment variable %s is not set", varName)
-		}
-		return value, nil
+		// The single placeholder was replaced with a single sentinel
+		// Return the mapped value directly
+		for _, value := range mapping {
+			return value, nil
+		}
 	}
pkg/database/database_test.go (1)

620-669: Test cases use old numeric sentinel format.

The Test_expandWithMapping test cases still use bare numeric strings like "999000" as mapping keys. While this works (the function just does string replacement), it doesn't reflect how the function is actually used with the new _PH-999XXX_ format.

Consider updating the test cases to use the actual sentinel format for consistency:

 		{
 			name:  "Single sentinel",
-			input: "user:999000",
+			input: "user:_PH-999000_",
 			mapping: map[string]string{
-				"999000": "secret",
+				"_PH-999000_": "secret",
 			},
 			want: "user:secret",
 		},
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 922ea74 and b7f8284.

📒 Files selected for processing (2)
  • pkg/database/database.go (5 hunks)
  • pkg/database/database_test.go (5 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.go: Error handling: use fmt.Errorf with contextual messages; check specific errors with errors.Is
Organize imports: standard library first, then third-party, then project imports; alphabetize within each group
Naming: CamelCase for exported identifiers; camelCase for unexported; preserve acronyms like ID, URL, HTTP, API
Limit line length to a maximum of 200 characters
Comments for exported items must be complete sentences ending with periods
Do not use log.Fatal or log.Panic (ruleguard-enforced)

Files:

  • pkg/database/database_test.go
  • pkg/database/database.go
**/*_test.go

📄 CodeRabbit inference engine (CLAUDE.md)

Tests should be table-driven using testify/require; name tests TestStructName_methodName

Files:

  • pkg/database/database_test.go
🪛 GitHub Check: go-lint
pkg/database/database.go

[failure] 292-292:
offBy1: Index() can return -1; maybe you wanted to do sentinelDSN[:schemeEnd+1] (gocritic)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: go-test (ubuntu-latest)
🔇 Additional comments (7)
pkg/database/database.go (5)

71-101: LGTM! Sentinel format change improves collision avoidance.

The new _PH-999XXX_ format with prefix and suffix is more robust than bare numeric sentinels. The counter-based approach ensures uniqueness within a single DSN.


103-112: LGTM!

The expandWithMapping function correctly replaces all sentinel occurrences with their mapped values.


151-174: LGTM! Good handling of embedded paths in host values.

The logic correctly splits host values containing paths (e.g., localhost:3306/dbname) and prepends the extracted path to the existing URL path.


288-300: Potential panic if scheme sentinel is the entire DSN without ://.

The static analysis correctly flags that strings.Index can return -1. If sentinelDSN contains :// but starts with a sentinel that doesn't have :// immediately after (edge case), or if the logic is reached via an unexpected path, slicing with schemeEnd could cause issues. However, the condition on line 290 guards this.

More critically: if schemeSentinel (line 292) extracts content before ://, but the sentinel itself contains :// in its value, the replacement on line 297 would be incorrect.

Consider adding a test case where the scheme environment variable value contains :// to verify correct behavior:

{
    name: "Scheme value contains colon-slash-slash",
    env: map[string]string{
        "DB_SCHEME": "mysql://injected",
    },
    dsn:     "${DB_SCHEME}://admin:1234@localhost:3306/dbname",
    want:    "", // or define expected behavior
    wantErr: true, // should this be rejected?
}

213-254: No action needed: IPv6 handling is correct for valid RFC 3986 URLs.

The expandPortSentinel function correctly identifies the port separator for bracketed IPv6 addresses like [::1]:_PH-999000_ because the last colon appears after the closing bracket. Unbracketed IPv6 addresses (e.g., ::1:_PH-999000_) are invalid according to RFC 3986 and would fail validation at the url.Parse call immediately following this function (line 306), making this a non-issue for the function's intended use.

pkg/database/database_test.go (2)

287-295: LGTM! Good test coverage for port placeholder.

This test case validates the expandPortSentinel functionality with both password and port placeholders in the same DSN, including a literal pattern (db-999001) that could collide with old numeric sentinels.


553-582: LGTM! Sentinel format updates align with implementation.

The test expectations correctly reflect the new _PH-999XXX_ sentinel format.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
pkg/database/database.go (2)

86-98: Misleading comment on error path.

The comment at line 88 states the sentinel is returned "to allow URL parsing for better error messages," but since the function returns early at line 96-98 when err != nil, URL parsing never occurs on error. The sentinel return is necessary for ReplaceAllStringFunc to build the result string, but the comment suggests a different purpose.

Consider updating the comment:

 		if !exists {
 			err = errors.Join(err, fmt.Errorf("environment variable %s is not set", varName))
-			return sentinel // Return sentinel anyway to allow URL parsing for better error messages
+			return sentinel // Return placeholder so ReplaceAllStringFunc can complete; err causes early exit below
 		}

213-254: Implementation is correct; consider documenting the side effect more prominently.

The port sentinel expansion logic correctly handles:

  • IPv6 addresses (uses LastIndex to find port separator after ])
  • Userinfo with colons (checks colonIndex > atIndex)
  • Various DSN structures

However, the function modifies the mapping parameter in place (line 248). While this is noted in the inline comment, the function signature and doc comment don't make this mutation clear to callers.

Consider updating the doc comment:

 // expandPortSentinel expands the port sentinel in the DSN before URL parsing.
 // Ports must be numeric for URL parsing, so we detect and expand port sentinels
 // (which use _PH-999000_ format) before passing to url.Parse.
-// The mapping is modified in place to remove the port sentinel.
+// Note: This function mutates the mapping parameter, removing the consumed port sentinel.
 func expandPortSentinel(sentinelDSN string, mapping map[string]string) string {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b7f8284 and 05d9666.

📒 Files selected for processing (1)
  • pkg/database/database.go (5 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.go: Error handling: use fmt.Errorf with contextual messages; check specific errors with errors.Is
Organize imports: standard library first, then third-party, then project imports; alphabetize within each group
Naming: CamelCase for exported identifiers; camelCase for unexported; preserve acronyms like ID, URL, HTTP, API
Limit line length to a maximum of 200 characters
Comments for exported items must be complete sentences ending with periods
Do not use log.Fatal or log.Panic (ruleguard-enforced)

Files:

  • pkg/database/database.go
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: go-test (ubuntu-latest)
🔇 Additional comments (4)
pkg/database/database.go (4)

103-112: LGTM!

The comment update accurately documents the sentinel format, and the function logic remains correct.


159-172: LGTM with one clarification.

The path handling logic is correct. When an environment variable contains both host and path (e.g., localhost:3306/dbname), the path is properly extracted and prepended to any existing path.

One edge case worth noting: if the existing parsedUrl.Path doesn't start with /, the concatenation at line 167 could produce unexpected results. However, url.Parse should always produce paths starting with / for absolute URLs, so this is likely safe.


275-286: Correct handling of single-variable DSN; minor redundancy noted.

This special case correctly handles DSNs that are entirely a single environment variable (e.g., ${FULL_DSN}), avoiding unnecessary URL parsing.

Minor observation: the os.LookupEnv at line 281 is redundant since extractPlaceholders already looked up the value and stored it in mapping. Since sentinelDSN equals the sentinel when the DSN is a single variable, you could use mapping[sentinelDSN] instead. However, this is a minor optimization and the current approach is clear and correct.


288-305: LGTM!

The scheme sentinel expansion (lines 288-302) and port sentinel expansion (line 305) correctly handle edge cases where these components are environment variables. Expanding these before url.Parse ensures the URL parser receives valid scheme and port formats.

@ggreer
ggreer merged commit 2556e6f into main Dec 2, 2025
4 checks passed
@ggreer
ggreer deleted the bugfix/address_expand-dsn_edge_cases branch December 2, 2025 22:30
Comment thread pkg/database/database.go
matches := DSNREnvRegex.FindAllString(dsn, -1)
if len(matches) == 1 && strings.TrimSpace(dsn) == strings.TrimSpace(matches[0]) {
// Get the variable name
varName := matches[0][2 : len(matches[0])-1]

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.

would we want to use the trimmed version of matches since we're trimming in the comparison?

Comment thread pkg/database/database.go
if strings.HasPrefix(portPart, "_PH-") && strings.HasSuffix(portPart, "_") {
if value, ok := mapping[portPart]; ok {
// Replace port sentinel with its value before parsing
beforePort := sentinelDSN[:schemeEnd+3+colonIndex+1]

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.

Maybe make a nicely named variable for schemeEnd+3 since it's repeated in multiple places and give maybe an example of what's being tested here in a comment. I don't actually ever see schemeEnd ever being used without adding the :// length to it so maybe just add it when you create that variable.

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.

3 participants