Skip to content

Conversation

@dlevy-msft-sql
Copy link
Contributor

@dlevy-msft-sql dlevy-msft-sql commented Jan 25, 2026

Summary

Implements the :serverlist interactive command to list available SQL Server instances via the SQL Browser service.

Changes

  • Add :serverlist command that queries the SQL Browser service (UDP port 1434)
  • Move server listing logic from cmd/sqlcmd/sqlcmd.go to pkg/sqlcmd/serverlist.go for reuse
  • Both -L flag and :serverlist command now use the shared ListLocalServers() function
  • Add comprehensive tests for parsing SQL Browser responses

Usage

# From command line
sqlcmd -L

# In interactive mode
1> :serverlist

Motivation

ODBC sqlcmd supports the :serverlist command to discover SQL Server instances. This brings go-sqlcmd to parity with that functionality.

Testing

  • Added unit tests for parseInstances() function
  • Added integration test for :serverlist command
  • All existing tests pass

- 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).
Copy link

Copilot AI left a 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 :serverlist command that queries the SQL Browser service on UDP port 1434
  • Refactored server listing logic from cmd/sqlcmd/sqlcmd.go to pkg/sqlcmd/serverlist.go for code reuse
  • Both the -L command-line flag and :serverlist command now share the ListLocalServers() 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
Copy link

Copilot AI left a 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
Copy link

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

Copy link

Copilot AI left a 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
Copy link

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

Copy link

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

Comment on lines 43 to 52
_, 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
}
Copy link

Copilot AI Jan 25, 2026

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.

Copilot uses AI. Check for mistakes.
@dlevy-msft-sql dlevy-msft-sql self-assigned this Jan 25, 2026
@dlevy-msft-sql dlevy-msft-sql added sqlcmd switch switch in existing sqlcmd Size: S Small issue (less than one week effort) labels Jan 25, 2026
- 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
Copy link

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

Comment on lines +162 to +163
MYSERVER\SQL2019
MYSERVER\SQL2022
Copy link

Copilot AI Jan 25, 2026

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.

Suggested change
MYSERVER\SQL2019
MYSERVER\SQL2022
MYSERVER\SQL2019
MYSERVER\SQL2022

Copilot uses AI. Check for mistakes.
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
Copy link

Copilot AI Jan 25, 2026

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

Suggested change
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)

Copilot uses AI. Check for mistakes.
func ListLocalServers(w io.Writer) {
instances, err := GetLocalServerInstances()
if err != nil {
fmt.Fprintln(os.Stderr, err)
Copy link

Copilot AI Jan 25, 2026

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.

Suggested change
fmt.Fprintln(os.Stderr, err)
fmt.Fprintln(w, err)

Copilot uses AI. Check for mistakes.
if len(instanceDict) == 0 {
break
}
// Only add if InstanceName key exists and is non-empty
Copy link

Copilot AI Jan 25, 2026

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.

Suggested change
// 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.

Copilot uses AI. Check for mistakes.
Comment on lines +37 to +76
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")
})
}
Copy link

Copilot AI Jan 25, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +162 to +163
MYSERVER\SQL2019
MYSERVER\SQL2022
Copy link

Copilot AI Jan 25, 2026

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".

Suggested change
MYSERVER\SQL2019
MYSERVER\SQL2022
(local)
MYSERVER
MYSERVER\SQL2019

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Size: S Small issue (less than one week effort) sqlcmd switch switch in existing sqlcmd

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant