-
Notifications
You must be signed in to change notification settings - Fork 79
Implement :serverlist command #630
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
- Add serverlist command to list SQL Server instances via SQL Browser service - Move server listing logic from cmd/sqlcmd to pkg/sqlcmd for reuse - Both -L flag and :serverlist command now use shared ListLocalServers function - Add comprehensive tests for serverlist functionality
Remove empty conditional branches that triggered staticcheck SA9003. Remove unused imports (errors, os).
There was a problem hiding this comment.
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 implements the :serverlist interactive command to discover SQL Server instances via the SQL Browser service, bringing go-sqlcmd to feature parity with ODBC sqlcmd.
Changes:
- Added
:serverlistcommand that queries the SQL Browser service on UDP port 1434 - Refactored server listing logic from
cmd/sqlcmd/sqlcmd.gotopkg/sqlcmd/serverlist.gofor code reuse - Both the
-Lcommand-line flag and:serverlistcommand now share theListLocalServers()function
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| pkg/sqlcmd/serverlist.go | New file containing refactored server listing functions (ListLocalServers, GetLocalServerInstances, parseInstances) |
| pkg/sqlcmd/serverlist_test.go | New test file with comprehensive tests for server listing and parsing functionality |
| pkg/sqlcmd/commands.go | Added SERVERLIST command registration and serverlistCommand handler function |
| cmd/sqlcmd/sqlcmd.go | Removed listLocalServers and parseInstances functions (moved to pkg/sqlcmd), updated -L flag to use sqlcmd.ListLocalServers() |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Add the :help command to display available sqlcmd commands. This improves compatibility with legacy ODBC sqlcmd. Changes: - Added HELP command to command registry - Added helpCommand function with full command list - Added tests for command parsing and functionality - Updated README.md
- Rename variables to follow Go naming conventions: - out_s -> outStr - got_name -> gotName - instdict -> instanceDict - Add argument validation to serverlistCommand
- Remove :serverlist and :perftrace from help text - These commands are in separate PRs and not yet merged - Help text should only list commands that exist in this branch
There was a problem hiding this comment.
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 6 out of 6 changed files in this pull request and generated 6 comments.
- Fix fmt.Fprintf spacing in ListLocalServers - Sort instance names for deterministic output - Add validation for missing ServerName in instances - Add argument validation for helpCommand - Add test cases for :SERVERLIST and :serverlist - Add assertion for :serverlist in TestHelpCommand
There was a problem hiding this comment.
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 6 out of 6 changed files in this pull request and generated 5 comments.
There was a problem hiding this comment.
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 6 out of 6 changed files in this pull request and generated 1 comment.
- Use strings.TrimSpace in serverlistCommand for consistency - Fix test formatting to put each test case on its own line - Add InstanceName validation before using as map key - Document :serverlist command in README.md
There was a problem hiding this comment.
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 6 out of 6 changed files in this pull request and generated no new comments.
There was a problem hiding this comment.
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 6 out of 6 changed files in this pull request and generated 1 comment.
| _, err = conn.Write(bmsg) | ||
| if err != nil { | ||
| // Silently ignore errors, same as ODBC | ||
| return nil | ||
| } | ||
| read, err := conn.Read(resp) | ||
| if err != nil { | ||
| // Silently ignore errors, same as ODBC | ||
| return nil | ||
| } |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error handling behavior has changed from the previous implementation. The old code in cmd/sqlcmd/sqlcmd.go printed non-timeout errors to stdout before returning. The new implementation silently ignores all errors. While the comment states this matches ODBC behavior, this represents a user-visible behavior change where network errors (connection refused, permission denied, etc.) that were previously visible to users are now completely silent. Consider whether this is the intended behavior or if at least some classes of errors should still be logged or returned.
- Changed GetLocalServerInstances() to return ([]string, error) - Only return error if NOT os.ErrDeadlineExceeded (timeout is expected) - ListLocalServers() prints errors to stderr (matches ODBC sqlcmd behavior) - Expanded README documentation for :serverlist command - Added batch script examples for error handling and automation
There was a problem hiding this comment.
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 6 out of 6 changed files in this pull request and generated 6 comments.
| MYSERVER\SQL2019 | ||
| MYSERVER\SQL2022 |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The example output shown here does not include the two-space indentation that the actual implementation produces. The code in ListLocalServers (serverlist.go:28) formats the output as " %s\n" with two leading spaces. The documentation should accurately show this formatting or the code should be updated to match the documentation.
| MYSERVER\SQL2019 | |
| MYSERVER\SQL2022 | |
| MYSERVER\SQL2019 | |
| MYSERVER\SQL2022 |
| To capture stderr separately (for error logging): | ||
| ```batch | ||
| sqlcmd -Q ":serverlist" 2>errors.log > servers.txt | ||
| if exist errors.log if not "%%~z errors.log"=="0" type errors.log |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This batch script has a syntax error. The file size test operator %~z only works within a FOR loop context, not directly in an IF statement. A correct alternative would be to use FOR /F to check the file size, or simply check if the file exists and has content using: "if exist errors.log (for %%A in (errors.log) do if %%~zA GTR 0 type errors.log)"
| if exist errors.log if not "%%~z errors.log"=="0" type errors.log | |
| if exist errors.log (for %%A in (errors.log) do if %%~zA GTR 0 type errors.log) |
| func ListLocalServers(w io.Writer) { | ||
| instances, err := GetLocalServerInstances() | ||
| if err != nil { | ||
| fmt.Fprintln(os.Stderr, err) |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Writing errors directly to os.Stderr creates an inconsistency with the rest of the sqlcmd architecture. When serverlistCommand calls ListLocalServers with s.GetOutput(), errors will bypass the configured error stream (s.GetError()). Consider either: (1) accepting an error writer parameter alongside the output writer, or (2) returning the error and letting the caller handle it. This would make error handling consistent with other commands like connectCommand which use s.GetError() or return errors to be handled by the framework.
| fmt.Fprintln(os.Stderr, err) | |
| fmt.Fprintln(w, err) |
| if len(instanceDict) == 0 { | ||
| break | ||
| } | ||
| // Only add if InstanceName key exists and is non-empty |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The validation added here (checking if InstanceName exists and is non-empty) is a good improvement over the old implementation. However, consider adding a comment explaining why instances without InstanceName should be skipped, as this represents a defensive measure against malformed SQL Browser responses.
| // Only add if InstanceName key exists and is non-empty | |
| // Only add if InstanceName key exists and is non-empty. | |
| // This defensively skips malformed or partial SQL Browser responses | |
| // that do not include a valid InstanceName for an instance. |
| func TestParseInstances(t *testing.T) { | ||
| // Test parsing of SQL Browser response | ||
| // Format: 0x05 (response type), 2 bytes length, then semicolon-separated key=value pairs | ||
| // Each instance ends with two semicolons | ||
|
|
||
| t.Run("empty response", func(t *testing.T) { | ||
| result := parseInstances([]byte{}) | ||
| assert.Empty(t, result) | ||
| }) | ||
|
|
||
| t.Run("invalid header", func(t *testing.T) { | ||
| result := parseInstances([]byte{1, 0, 0}) | ||
| assert.Empty(t, result) | ||
| }) | ||
|
|
||
| t.Run("valid single instance", func(t *testing.T) { | ||
| // Simulating SQL Browser response format | ||
| // Header: 0x05 followed by 2 length bytes, then the instance data | ||
| data := []byte{5, 0, 0} | ||
| instanceData := "ServerName;MYSERVER;InstanceName;MSSQLSERVER;IsClustered;No;Version;15.0.2000.5;tcp;1433;;" | ||
| data = append(data, []byte(instanceData)...) | ||
|
|
||
| result := parseInstances(data) | ||
| assert.Len(t, result, 1) | ||
| assert.Contains(t, result, "MSSQLSERVER") | ||
| assert.Equal(t, "MYSERVER", result["MSSQLSERVER"]["ServerName"]) | ||
| assert.Equal(t, "1433", result["MSSQLSERVER"]["tcp"]) | ||
| }) | ||
|
|
||
| t.Run("valid multiple instances", func(t *testing.T) { | ||
| data := []byte{5, 0, 0} | ||
| instanceData := "ServerName;MYSERVER;InstanceName;MSSQLSERVER;tcp;1433;;ServerName;MYSERVER;InstanceName;SQLEXPRESS;tcp;1434;;" | ||
| data = append(data, []byte(instanceData)...) | ||
|
|
||
| result := parseInstances(data) | ||
| assert.Len(t, result, 2) | ||
| assert.Contains(t, result, "MSSQLSERVER") | ||
| assert.Contains(t, result, "SQLEXPRESS") | ||
| }) | ||
| } |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider adding a test case for an instance with a missing or empty ServerName to verify that the new validation logic in parseInstances correctly filters out such instances. This would test the defensive behavior added in lines 109-112 of serverlist.go.
| MYSERVER\SQL2019 | ||
| MYSERVER\SQL2022 |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The example output doesn't show the behavior when the default MSSQLSERVER instance is present. According to the code (serverlist.go:82-83), the default instance produces two entries in the output: "(local)" and the actual server name (e.g., "MYSERVER"). Consider adding an example showing this case, such as " (local)\n MYSERVER\n MYSERVER\SQL2019".
| MYSERVER\SQL2019 | |
| MYSERVER\SQL2022 | |
| (local) | |
| MYSERVER | |
| MYSERVER\SQL2019 |
Summary
Implements the
:serverlistinteractive command to list available SQL Server instances via the SQL Browser service.Changes
:serverlistcommand that queries the SQL Browser service (UDP port 1434)cmd/sqlcmd/sqlcmd.gotopkg/sqlcmd/serverlist.gofor reuse-Lflag and:serverlistcommand now use the sharedListLocalServers()functionUsage
Motivation
ODBC sqlcmd supports the
:serverlistcommand to discover SQL Server instances. This brings go-sqlcmd to parity with that functionality.Testing
parseInstances()function:serverlistcommand