Skip to content

Improve DSN env vairable expansion - #92

Merged
pquerna merged 4 commits into
mainfrom
pq/dsn_expand_env
Nov 21, 2025
Merged

Improve DSN env vairable expansion#92
pquerna merged 4 commits into
mainfrom
pq/dsn_expand_env

Conversation

@pquerna

@pquerna pquerna commented Nov 20, 2025

Copy link
Copy Markdown
Contributor
  • Make a more robust method of injecting environment variables into the DSN. Handles when passwords might contain invalid characters for the URL, along with a ton of tests.

Summary by CodeRabbit

  • Bug Fixes
    • Safer, component-wise expansion of environment variables in database connection strings: placeholder-based three-phase processing preserves URL structure and encoding, supports credentials in a single variable, and robustly handles hosts (including IPv6), ports, paths, queries, fragments, and missing/nested variables.
    • Connection API now accepts a structured options object for supplying DSN or explicit components.
  • Tests
    • Added comprehensive tests covering DSN expansion scenarios, encoding, and error cases.

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

Copilot AI review requested due to automatic review settings November 20, 2025 20:24
@coderabbitai

coderabbitai Bot commented Nov 20, 2025

Copy link
Copy Markdown

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds a three-phase DSN expansion pipeline: replace ${VAR} placeholders with sentinels (resolving env vars), parse the DSN safely, then expand sentinels into URL components (userinfo, host, path, query, fragment). Connect now accepts a ConnectOptions struct and calls expandDSN before building the final connection URL.

Changes

Cohort / File(s) Change Summary
Database core & DSN logic
pkg/database/database.go
Introduces ConnectOptions public type and changes Connect signature to Connect(ctx, opts ConnectOptions). Adds three-phase DSN expansion flow and helpers: extractPlaceholders, expandWithMapping, expandDSN, buildConnectionURL, and per-component expanders (expandUserInfo, expandHost, expandPath, expandQuery, expandFragment). Implements sentinel-based placeholder handling and per-component encoding.
Tests for DSN expansion
pkg/database/database_test.go
Adds extensive tests: Test_expandDSN, Test_extractPlaceholders, Test_expandWithMapping, Test_buildConnectionURL covering env substitution, encoding, IPv6/port cases, combined user:pass envs, literal braces, missing vars, and assembling connection URLs.
Config struct additions
pkg/bsql/config.go
Extends DatabaseConfig with structured override fields: Scheme, Host, Port, Database, and Params to allow explicit configuration alongside DSN.
Callsite update
pkg/connector/connector.go
Replaces Connect(ctx, dsn, user, password) calls with construction of ConnectOptions and Connect(ctx, opts), passing DSN and structured fields (user/password/params/etc.).

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant Connector
    participant DBPkg as "database.Connect"
    participant expandDSN
    participant extractPH as "extractPlaceholders"
    participant urlParse as "url.Parse"
    participant CompExpand as "expandUser/Host/Path/Query/Frag"
    participant buildURL as "buildConnectionURL"

    Caller->>Connector: build ConnectOptions and call Connect(opts)
    Connector->>DBPkg: Connect(ctx, opts)
    DBPkg->>expandDSN: expandDSN(opts.DSN)
    expandDSN->>extractPH: extractPlaceholders(dsn)
    extractPH-->>expandDSN: safeDSN + mapping
    expandDSN->>urlParse: parse safeDSN
    urlParse-->>expandDSN: parsedURL
    rect rgb(235,245,255)
      Note over expandDSN,CompExpand: Component-wise expansion (encoding-aware)
      expandDSN->>CompExpand: expandUserInfo/Host/Path/Query/Fragment(parsedURL, mapping)
      CompExpand-->>expandDSN: expanded components
    end
    expandDSN->>buildURL: buildConnectionURL(parsedURL, opts, mapping)
    buildURL-->>DBPkg: final connection URL
    DBPkg-->>Caller: established DB connection
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Inspect URL component encoding/decoding in expandUserInfo, expandHost, expandQuery.
  • Verify sentinel uniqueness, mapping correctness and edge-case handling in extractPlaceholders.
  • Review buildConnectionURL precedence between DSN and structured ConnectOptions fields and error conditions in tests.

Possibly related PRs

Suggested reviewers

  • laurenleach
  • btipling

Poem

🐰 I hid small sentinels in a string so neat,
Then parsed each lane and warmed each seat,
User and host, path and query too,
I stitched them back encoded and true,
Hooray — the DSN hops home anew! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.37% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title contains a typo ('vairable' instead of 'variable'), making it appear unclear or potentially non-descriptive, though the intent is understandable. Correct the typo to 'Improve DSN env variable expansion' for clarity and professionalism.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ 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 pq/dsn_expand_env

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

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

Pull Request Overview

This PR introduces a more robust method for expanding environment variables in database DSN (Data Source Name) strings, specifically handling special characters that could break URL parsing. The improvement replaces the previous updateFromEnv function with a new three-phase expandDSN approach that uses sentinel values to safely parse URLs before expanding environment variables.

Key Changes:

  • Implements a sentinel-based URL expansion system that prevents special characters in environment variables from breaking DSN parsing
  • Adds component-specific expansion functions for different URL parts (user info, host, path, query, fragment) with appropriate encoding
  • Includes comprehensive test coverage with 67 test cases covering edge cases like special characters, Unicode, and various database types

Reviewed Changes

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

File Description
pkg/database/database.go Adds new expandDSN function and helper functions for sentinel-based URL expansion; updates Connect function to use new expansion method
pkg/database/database_test.go Adds comprehensive test suites for expandDSN, extractPlaceholders, and expandWithMapping functions

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pkg/database/database.go Outdated
Comment on lines +54 to +57
// extractPlaceholders replaces ${...} placeholders with unique sentinels that are URL-safe.
// This allows us to parse the URL structure before expanding environment variables.
// Returns: the string with sentinels, a mapping of sentinel->varName, and any error.
func extractPlaceholders(s string) (string, map[string]string, error) {

Copilot AI Nov 20, 2025

Copy link

Choose a reason for hiding this comment

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

The function always returns nil for the error but has error in the return signature. Either remove the error return value or add error handling logic.

Copilot uses AI. Check for mistakes.
Comment thread pkg/database/database.go Outdated
Comment on lines +79 to +86
if strings.Contains(result, sentinel) {
value, exists := os.LookupEnv(varName)
if !exists {
err = errors.Join(err, fmt.Errorf("environment variable %s is not set", varName))
continue
}
result = strings.ReplaceAll(result, sentinel, value)
}

Copilot AI Nov 20, 2025

Copy link

Choose a reason for hiding this comment

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

The strings.Contains check is redundant since strings.ReplaceAll will simply return the original string if the sentinel is not found. Removing this check simplifies the code without changing behavior.

Suggested change
if strings.Contains(result, sentinel) {
value, exists := os.LookupEnv(varName)
if !exists {
err = errors.Join(err, fmt.Errorf("environment variable %s is not set", varName))
continue
}
result = strings.ReplaceAll(result, sentinel, value)
}
value, exists := os.LookupEnv(varName)
if !exists {
err = errors.Join(err, fmt.Errorf("environment variable %s is not set", varName))
continue
}
result = strings.ReplaceAll(result, sentinel, value)

Copilot uses AI. Check for mistakes.

@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 (3)
pkg/database/database.go (2)

171-184: Note on query expansion and interpretation of special characters.

expandQuery writes the expanded string directly into parsedUrl.RawQuery, so any &, = or similar characters coming from env values are treated as full query syntax (splitting into multiple params), not encoded as part of a single parameter value. That matches your Query parameters with special chars / Ampersand in query parameter value tests, but it’s worth being explicit in docs/comments so callers don’t assume value‑only semantics.


246-274: Consider adding more contextual error wrapping in Connect.

Connect now relies on expandDSN, which can fail for missing env vars or malformed DSNs, but errors are returned as‑is. Per your guidelines about contextual errors, wrapping here would make debugging easier, e.g.:

-	populatedDSN, err := expandDSN(dsn)
-	if err != nil {
-		return nil, Unknown, err
-	}
+	populatedDSN, err := expandDSN(dsn)
+	if err != nil {
+		return nil, Unknown, fmt.Errorf("expanding DSN: %w", err)
+	}
@@
-	parsedDsn, err := url.Parse(populatedDSN)
-	if err != nil {
-		return nil, Unknown, err
-	}
+	parsedDsn, err := url.Parse(populatedDSN)
+	if err != nil {
+		return nil, Unknown, fmt.Errorf("parsing DSN: %w", err)
+	}
pkg/database/database_test.go (1)

86-570: Optional: consider testify/require for clearer test assertions.

The tests are already table-driven and readable, but your guidelines call out testify/require. Migrating the assertions (e.g., require.NoError, require.Equal) would improve failure messages and keep the subtest bodies a bit leaner, if you decide to align fully with that style.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8307960 and 2eb47c5.

📒 Files selected for processing (2)
  • pkg/database/database.go (2 hunks)
  • pkg/database/database_test.go (1 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
🧬 Code graph analysis (1)
pkg/database/database.go (1)
pkg/database/mysql/mysql.go (1)
  • Connect (47-59)
⏰ 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 (8)
pkg/database/database.go (5)

54-70: Sentinel-based placeholder extraction looks correct and deterministic.

extractPlaceholders cleanly replaces ${VAR} with URL-safe sentinels and tracks them in a mapping; tests cover multiple and repeated vars, and the use of ReplaceAllStringFunc with a monotonic counter preserves left‑to‑right ordering as expected.


72-94: expandWithMapping error semantics are reasonable and match tests.

The function correctly:

  • Replaces only present sentinels in the input.
  • Aggregates missing‑env errors via errors.Join.
  • Returns an empty string on any error, avoiding partial DSN expansion (as your tests assert).

96-139: Userinfo expansion correctly handles both split and combined credentials.

The logic to:

  • Expand username and password separately via expandWithMapping, and
  • Treat a single ${CREDENTIALS} as user:password (with support for extra colons in the password, encoded by url.UserPassword),

matches the test expectations for “entire userinfo as single variable” and “multiple colons” cases.


141-199: Host/path/fragment expansions are straightforward and component-scoped.

Each helper expands placeholders only when the component is non‑empty and writes back to the parsed url.URL, letting url.URL.String() handle encoding (host/backslash, path with slashes, fragments, etc.), which aligns with the edge‑case tests (IPv6 host, SQL Server named instance, path with /, fragment variable).


201-244: Three-phase expandDSN pipeline is robust and well-structured.

The sentinel → parse → component‑wise expansion flow:

  • Avoids URL-parse breakage from raw secrets (#, @, :, %, unicode, etc.).
  • Provides a clear error for invalid DSN structure (fmt.Errorf("invalid DSN structure: %w", err)).
  • Properly bails out early when there are no placeholders, preserving the original DSN.

Overall this is a solid improvement over direct ${VAR} inlining.

pkg/database/database_test.go (3)

86-447: Comprehensive DSN expansion tests; semantics align with the implementation.

The Test_expandDSN table covers:

  • No placeholders, simple substitutions, repeated vars.
  • Userinfo with many special chars (including unicode and emoji), combined user:pass, and multiple colons.
  • Host, path, port, fragment, and various schemes (mysql/postgres/sqlserver/oracle/hdb).
  • Edge cases like sentinel collisions, % literals, IPv6, and characters that should/shouldn’t be URL-escaped.

This gives strong confidence in the new pipeline and nicely documents intended behaviors (especially around how query strings and userinfo are interpreted).


449-497: extractPlaceholders tests validate ordering and mapping cardinality well.

The cases for no placeholders, single, multiple, and repeated variables assert both the transformed string and the mapping size, which matches how extractPlaceholders assigns incrementing __PH_n__ sentinels.


499-570: expandWithMapping tests cover both success and missing-env error paths.

These cases exercise:

  • No sentinels (no-op).
  • Single and multiple sentinel expansions with env-backed values.
  • The missing-variable scenario, where wantErr is true.

That matches the implementation’s contract and ensures errors from os.LookupEnv are surfaced.

@ggreer ggreer 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.

This breaks port numbers in variables.

Comment thread pkg/database/database.go Outdated
Comment thread pkg/database/database.go Outdated

@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: 1

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

86-455: Consider using testify/require for assertions.

The coding guidelines specify that tests should use testify/require, but these tests use standard library testing assertions. Consider refactoring to use require.NoError, require.Equal, etc., for more expressive assertions and better error messages.

As per coding guidelines

Example refactor for one test case:

+import (
+	"github.com/stretchr/testify/require"
+)
+
 func Test_expandDSN(t *testing.T) {
 	// ... test cases ...
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			for k, v := range tt.env {
 				t.Setenv(k, v)
 			}
 
 			got, err := expandDSN(tt.dsn)
-			if (err != nil) != tt.wantErr {
-				t.Errorf("expandDSN() error = %v, wantErr %v", err, tt.wantErr)
-				return
-			}
-			if got != tt.want {
-				t.Errorf("expandDSN() got = %v, want %v", got, tt.want)
-			}
+			if tt.wantErr {
+				require.Error(t, err)
+			} else {
+				require.NoError(t, err)
+				require.Equal(t, tt.want, got)
+			}
 		})
 	}
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2eb47c5 and a73c63d.

📒 Files selected for processing (1)
  • pkg/database/database_test.go (1 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
**/*_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
⏰ 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 (3)
pkg/database/database_test.go (3)

441-443: Good use of t.Setenv for test isolation.

The tests correctly use t.Setenv for environment variable setup, which automatically cleans up after each test. This is cleaner and more reliable than manual cleanup with os.Unsetenv.


259-435: Excellent edge case coverage.

The test suite includes comprehensive edge cases covering security concerns (sentinel collision, injection attempts), internationalization (Unicode, emoji), control characters (newline, tab), and various database-specific formats (IPv6, SQL Server backslash notation). This demonstrates thorough testing of the DSN expansion implementation.


232-238: Remove duplicate test case.

The test cases "Empty password" (lines 232-238) and "Empty password (explicitly set)" (lines 389-395) are identical in their environment variables, DSN input, and expected output.

Apply this diff to remove the duplicate:

-	{
-		name: "Empty password (explicitly set)",
-		env: map[string]string{
-			"DB_PASSWORD": "",
-		},
-		dsn:  "mysql://user:${DB_PASSWORD}@localhost/db",
-		want: "mysql://user:@localhost/db",
-	},

Also applies to: 389-395

⛔ Skipped due to learnings
Learnt from: CR
Repo: ConductorOne/baton-sql PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-27T00:53:43.641Z
Learning: Applies to **/*_test.go : Tests should be table-driven using testify/require; name tests `TestStructName_methodName`

Comment thread pkg/database/database_test.go
Copilot AI review requested due to automatic review settings November 21, 2025 05:59

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

Pull Request Overview

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


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pkg/database/database.go
Comment thread pkg/database/database.go

@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 (4)
pkg/database/database.go (1)

214-219: Minor: Ensure comment ends with a period per coding guidelines.

The switch to expandDSN correctly implements the new robust expansion approach. However, the comment at line 215 should end with a period to comply with the coding guideline requiring complete sentences.

Apply this diff:

-	// Use the new expandDSN function which handles special characters correctly
+	// Use the new expandDSN function which handles special characters correctly.
 	populatedDSN, err := expandDSN(dsn)

As per coding guidelines.

pkg/database/database_test.go (3)

86-479: Consider using testify/require for consistency with coding guidelines.

The coding guidelines specify that "Tests should be table-driven using testify/require". While the test is table-driven and functionally correct, adopting testify/require would improve consistency and provide better assertion failure messages.

Example migration:

import (
	"github.com/stretchr/testify/require"
)

// In the test:
got, err := expandDSN(tt.dsn)
if tt.wantErr {
	require.Error(t, err)
	return
}
require.NoError(t, err)
require.Equal(t, tt.want, got)

As per coding guidelines.


481-568: Consider using testify/require for consistency with coding guidelines.

Same recommendation as for Test_expandDSN—adopting testify/require would align with the project's coding guidelines and provide clearer assertion failures.

As per coding guidelines.


570-619: Consider using testify/require for consistency with coding guidelines.

Same recommendation as for the other test functions—adopting testify/require would align with the project's coding guidelines.

As per coding guidelines.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 43b54f0 and 9d01224.

📒 Files selected for processing (2)
  • pkg/database/database.go (2 hunks)
  • pkg/database/database_test.go (1 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
🧬 Code graph analysis (1)
pkg/database/database.go (3)
pkg/database/hdb/hdb.go (1)
  • Connect (10-17)
pkg/database/mysql/mysql.go (1)
  • Connect (47-59)
pkg/database/oracle/oracle.go (1)
  • Connect (10-16)
⏰ 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 (11)
pkg/database/database.go (8)

11-11: LGTM!

The strings import is correctly placed and alphabetically ordered within the standard library imports group.


88-96: LGTM!

The function correctly performs sentinel-to-value replacement using strings.ReplaceAll. The implementation is clean and addresses the past review feedback about removing redundant checks.


98-133: LGTM! Smart handling of combined credentials.

The function correctly handles both separate username/password variables and the special case where a single variable contains "user:password" (lines 120-125). The use of url.UserPassword() ensures proper URL encoding of special characters like @, #, :, etc.

The logic correctly only triggers the colon-splitting behavior when no password component was present in the original DSN structure (!hasPass), avoiding false positives for legitimate usernames containing colons.


135-147: LGTM!

Both functions correctly perform simple sentinel replacement. The lack of additional encoding in expandPath is intentional and correct—path components like database names that contain slashes should be preserved as structural elements of the URL path rather than being percent-encoded.


149-170: LGTM! Proper query parameter encoding.

The function correctly handles query parameter expansion and encoding:

  1. Parses the query string to extract key-value pairs
  2. Expands sentinels in both keys and values
  3. Re-encodes using url.Values.Encode(), which properly percent-encodes special characters (e.g., & becomes %26, spaces become +)

The fallback at lines 154-157 is a reasonable defensive measure to preserve functionality if parsing unexpectedly fails.


172-177: LGTM!

Simple and correct implementation for fragment expansion.


179-212: LGTM! Well-structured three-phase expansion.

The function correctly orchestrates the DSN expansion pipeline:

  1. Phase 1: Extracts placeholders, looks up environment variables, and generates sentinels
  2. Phase 2: Parses the DSN with sentinels to establish URL structure safely
  3. Phase 3: Expands each component with appropriate encoding

The early return optimization (lines 193-196) avoids unnecessary parsing when no expansion is needed. Error handling follows the coding guidelines with contextual messages.


54-86: Verification complete: the 1000 placeholder limit is sufficient for production use.

The codebase test suite demonstrates a maximum of 5 placeholders in any single DSN string (e.g., ${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}). This is far below the 1000-placeholder capacity provided by the 999%03d sentinel format. DSN strings are inherently limited in length and complexity, making the limit a non-issue in practice.

The function's design—returning sentinels even when environment variables are missing—is intentional and well-reasoned, allowing URL parsing to succeed so error messages can identify which variables failed. The implementation is production-ready as-is.

pkg/database/database_test.go (3)

86-479: Excellent test coverage with comprehensive edge cases!

The test suite thoroughly validates the DSN expansion logic across numerous scenarios including:

  • Special character encoding in passwords and usernames (lines 101-140)
  • Port injection concerns (lines 109-115)
  • Combined credentials handling (lines 169-175)
  • Query parameter encoding (lines 185-199, 349-355)
  • Unicode and emoji support (lines 365-379)
  • Security scenarios like sentinel collision (lines 276-282)

The test expectations correctly encode ampersands as %26 in query parameter values (lines 190, 354), addressing the concern from previous reviews. The tests also correctly show that ampersands in passwords remain unencoded per RFC 3986 (line 346).


481-568: LGTM! Good coverage of sentinel generation logic.

The tests validate key behaviors:

  • Sentinel format (999000, 999001, etc.)
  • Mapping size correctness
  • Handling of repeated variables (lines 518-525)
  • Port placeholder validation (lines 527-534)
  • Error handling for missing environment variables (lines 536-542)

The test at lines 518-525 importantly confirms that the same variable used twice generates two distinct sentinels, which is necessary for the expansion logic.


570-619: LGTM! Adequate coverage for the helper function.

The tests appropriately validate the sentinel replacement logic including the edge case where a sentinel exists in the mapping but not in the input string (lines 602-608).

Copilot AI review requested due to automatic review settings November 21, 2025 22:50

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

Pull request overview

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


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pkg/database/database.go

@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)

71-103: Document the sentinel count limitation.

The sentinel format "999%03d" at line 83 limits the system to 1000 placeholders (000-999). While this is more than sufficient for typical DSN configurations, documenting this limitation in the function comment would be helpful for maintainability.

Consider adding to the comment:

// extractPlaceholders replaces ${...} placeholders with unique numeric sentinels
// (supports up to 1000 placeholders: 999000-999999) and looks up the environment
// variable values immediately.

282-407: Well-structured URL construction with proper override behavior.

The function correctly implements the precedence logic where structured fields override DSN components. The IPv6 handling (lines 342-344) properly strips and re-adds brackets via net.JoinHostPort, and the database path normalization ensures consistent leading slash handling.

While the function is 125 lines long, it's well-organized into logical sections (scheme, host/port, database, credentials, params), and the complexity is inherent to supporting both DSN and structured configurations with environment variable expansion.

If future maintainability becomes a concern, consider extracting host/port resolution (lines 309-351) into a helper function like resolveHostPort(parsedUrl *url.URL, host, port 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 9d01224 and 45f3cf1.

⛔ Files ignored due to path filters (1)
  • examples/example.yml is excluded by none and included by none
📒 Files selected for processing (4)
  • pkg/bsql/config.go (1 hunks)
  • pkg/connector/connector.go (1 hunks)
  • pkg/database/database.go (4 hunks)
  • pkg/database/database_test.go (1 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/bsql/config.go
  • pkg/database/database.go
  • pkg/connector/connector.go
  • pkg/database/database_test.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
🧬 Code graph analysis (3)
pkg/database/database.go (3)
pkg/database/hdb/hdb.go (1)
  • Connect (10-17)
pkg/database/mysql/mysql.go (1)
  • Connect (47-59)
pkg/database/oracle/oracle.go (1)
  • Connect (10-16)
pkg/connector/connector.go (2)
pkg/database/database.go (2)
  • ConnectOptions (37-49)
  • Connect (231-280)
pkg/database/mysql/mysql.go (1)
  • Connect (47-59)
pkg/database/database_test.go (1)
pkg/database/database.go (1)
  • ConnectOptions (37-49)
⏰ 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 (12)
pkg/connector/connector.go (1)

97-108: LGTM! Clean refactoring to use the new ConnectOptions API.

The construction of ConnectOptions properly maps all configuration fields from c.Connect to the new structured options format. This aligns well with the updated database.Connect signature.

pkg/bsql/config.go (1)

39-68: LGTM! Well-documented public API extension.

The additions to DatabaseConfig are comprehensive and well-commented. The documentation clearly explains the relationship between the DSN string and structured fields, including the override behavior and environment variable expansion syntax. All fields follow Go naming conventions and include appropriate yaml/json tags.

pkg/database/database_test.go (3)

86-479: Excellent test coverage for DSN expansion!

The test suite is comprehensive and well-structured, covering a wide range of scenarios including:

  • Multiple database schemes (MySQL, PostgreSQL, SQL Server, Oracle, HDB)
  • Special characters in credentials (# @ : / ? % etc.)
  • IPv6 addresses
  • Unicode and emoji characters
  • Query parameter encoding (properly expects %26 for ampersands)
  • Edge cases like sentinel collision and missing environment variables

The table-driven approach follows Go testing best practices.


481-568: LGTM! Good coverage of placeholder extraction logic.

The tests properly verify sentinel generation, mapping creation, and error handling for missing environment variables. The coverage includes edge cases like the same variable appearing multiple times.


621-724: LGTM! Thorough testing of URL construction logic.

The tests properly verify the URL building process including:

  • DSN-only configuration
  • Override behavior where structured fields take precedence
  • Environment variable expansion in structured fields
  • IPv6 address handling with proper bracket handling
  • Error conditions like port without host
pkg/database/database.go (7)

35-49: LGTM! Well-designed configuration struct.

The ConnectOptions struct provides a clean, flexible interface supporting both DSN strings and structured configuration. The documentation clearly explains that environment variable placeholders are supported in any field.


105-113: LGTM! Clean implementation of sentinel expansion.

The function efficiently replaces sentinels with their corresponding values using straightforward string replacement.


115-150: LGTM! Proper handling of credentials with special characters.

The function correctly handles URL encoding of special characters in userinfo components. The special case for combined "user:password" credentials (lines 137-142) properly splits on the first colon, and url.UserPassword() ensures all special characters are percent-encoded.


152-164: LGTM! Clean component-wise expansion helpers.

The expandHost and expandPath functions follow a consistent pattern, delegating to expandWithMapping for their respective URL components.


166-187: LGTM! Correct query parameter expansion with proper encoding.

The function properly expands sentinels in query parameters and uses url.Values.Encode() to ensure special characters (including ampersands and spaces) are correctly percent-encoded. The fallback handling for unparseable query strings preserves robustness.


196-229: LGTM! Elegant three-phase expansion solves URL parsing challenges.

The three-phase approach elegantly handles the core challenge: special characters in environment variables (like #, @, :) would break URL parsing if expanded directly. By using numeric sentinels as intermediaries, the URL structure is safely parsed before component-wise expansion with proper encoding.


409-417: LGTM! Clean helper for conditional environment variable expansion.

The function efficiently handles the three cases: empty strings, values with placeholders, and plain values without unnecessary processing.

@pquerna
pquerna merged commit 5879591 into main Nov 21, 2025
4 checks passed
@pquerna
pquerna deleted the pq/dsn_expand_env branch November 21, 2025 23:32
Comment thread pkg/database/database.go

// ConnectOptions represents the structured configuration used to build a DSN.
// Any field may include ${ENV_VAR} placeholders that will be expanded before use.
type ConnectOptions struct {

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.

Isn't ConnectOptions identical to DatabaseConfig? Why make two structs that have the exact same things in them? It's just losing the json & yaml tags.

Comment thread pkg/database/database.go
var err error

result := DSNREnvRegex.ReplaceAllStringFunc(s, func(match string) string {
sentinel := fmt.Sprintf("999%03d", counter)

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.

Won't this replace any value that contains three 9s followed by three more numbers? eg: If DB_PASSWORD is "1234" and the connection string is mysql://admin:${DB_PASSWORD}@localhost:3306/db-999000, then the final DSN will be mysql://admin:1234@localhost:3306/db-1234

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.

You can also run into this if you put certain values in environment variables. eg:

env: map[string]string{
	"DB_USER":     "user-999001",
	"DB_PASSWORD": "1234",
},
dsn:  "mysql://${DB_USER}:${DB_PASSWORD}@localhost:3306/dbname",

...will generate mysql://user-1234:1234@localhost:3306/dbname instead of mysql://user-999001:1234@localhost:3306/dbname

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.

This code can't replace the DB scheme in the DSN. eg: dsn: "${DB_SCHEME}://admin:1234@localhost:3306/dbname" will fail with invalid DSN structure: parse "999000://admin:1234@localhost:3306/dbname": first path segment in URL cannot contain colon.

It also can't replace env vars that span more than one part the DSN. eg: dsn: "${DSN}" with the DSN env var set to mysql://admin:1234@localhost:3306/dbname will return a DSN of ./mysql://admin:1234@localhost:3306/dbname.

Anthoer example is with the env var DB_ENDPOINT set to "localhost:3306/dbname" and a dsn in the config set to mysql://admin:password@${DB_ENDPOINT}. It evaluates to mysql://admin:password@localhost:3306%2Fdbname instead of mysql://admin:password@localhost:3306/dbname.

If anyone uses env vars in their configs like this, this PR will break their connector.

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.

4 participants