Skip to content

Commit 675a0df

Browse files
feat(open): add sqlcmd open vscode and sqlcmd open ssms commands
1 parent ca107b8 commit 675a0df

32 files changed

+1789
-20
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ linux-s390x/sqlcmd
3636
# Build artifacts in root
3737
/sqlcmd
3838
/sqlcmd_binary
39+
/modern
3940

4041
# certificates used for local testing
4142
*.der

README.md

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,18 +59,51 @@ The Homebrew package manager may be used on Linux and Windows Subsystem for Linu
5959

6060
Use `sqlcmd` to create SQL Server and Azure SQL Edge instances using a local container runtime (e.g. [Docker][] or [Podman][])
6161

62-
### Create SQL Server instance using local container runtime and connect using Azure Data Studio
62+
### Create SQL Server instance using local container runtime
6363

64-
To create a local SQL Server instance with the AdventureWorksLT database restored, query it, and connect to it using Azure Data Studio, run:
64+
To create a local SQL Server instance with the AdventureWorksLT database restored, run:
6565

6666
```
6767
sqlcmd create mssql --accept-eula --using https://aka.ms/AdventureWorksLT.bak
6868
sqlcmd query "SELECT DB_NAME()"
69-
sqlcmd open ads
7069
```
7170

7271
Use `sqlcmd --help` to view all the available sub-commands. Use `sqlcmd -?` to view the original ODBC `sqlcmd` flags.
7372

73+
### Connect using Visual Studio Code
74+
75+
Use `sqlcmd open vscode` to open Visual Studio Code with a connection profile configured for the current context:
76+
77+
```
78+
sqlcmd open vscode
79+
```
80+
81+
This command will:
82+
1. **Create a connection profile** in VS Code's user settings with the current context name
83+
2. **Copy the password to clipboard** so you can paste it when prompted
84+
3. **Launch VS Code** ready to connect
85+
86+
To also install the MSSQL extension (if not already installed), add the `--install-extension` flag:
87+
88+
```
89+
sqlcmd open vscode --install-extension
90+
```
91+
92+
Once VS Code opens, use the MSSQL extension's Object Explorer to connect using the profile. When you connect to the container, VS Code will automatically detect it as a Docker container and provide additional container management features (start/stop/delete) directly from the Object Explorer.
93+
94+
### Connect using SQL Server Management Studio (Windows)
95+
96+
On Windows, use `sqlcmd open ssms` to open SQL Server Management Studio pre-configured to connect to the current context:
97+
98+
```
99+
sqlcmd open ssms
100+
```
101+
102+
This command will:
103+
1. **Copy the password to clipboard** so you can paste it in the login dialog
104+
2. **Launch SSMS** with the server and username pre-filled
105+
3. You'll be prompted for the password - just paste from clipboard (Ctrl+V)
106+
74107
### The ~/.sqlcmd/sqlconfig file
75108

76109
Each time `sqlcmd create` completes, a new context is created (e.g. mssql, mssql2, mssql3 etc.). A context contains the endpoint and user configuration detail. To switch between contexts, run `sqlcmd config use <context-name>`, to view name of the current context, run `sqlcmd config current-context`, to list all contexts, run `sqlcmd config get-contexts`.

cmd/modern/root/open.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,21 @@ type Open struct {
1717
func (c *Open) DefineCommand(...cmdparser.CommandOptions) {
1818
options := cmdparser.CommandOptions{
1919
Use: "open",
20-
Short: localizer.Sprintf("Open tools (e.g Azure Data Studio) for current context"),
20+
Short: localizer.Sprintf("Open tools (e.g., Visual Studio Code, SSMS) for current context"),
2121
SubCommands: c.SubCommands(),
2222
}
2323

2424
c.Cmd.DefineCommand(options)
2525
}
2626

2727
// SubCommands sets up the sub-commands for `sqlcmd open` such as
28-
// `sqlcmd open ads`
28+
// `sqlcmd open ads`, `sqlcmd open vscode`, and `sqlcmd open ssms`
2929
func (c *Open) SubCommands() []cmdparser.Command {
3030
dependencies := c.Dependencies()
3131

3232
return []cmdparser.Command{
3333
cmdparser.New[*open.Ads](dependencies),
34+
cmdparser.New[*open.VSCode](dependencies),
35+
cmdparser.New[*open.Ssms](dependencies),
3436
}
3537
}

cmd/modern/root/open/ads_test.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,24 @@
44
package open
55

66
import (
7+
"runtime"
8+
"testing"
9+
710
"github.com/microsoft/go-sqlcmd/cmd/modern/sqlconfig"
811
"github.com/microsoft/go-sqlcmd/internal/cmdparser"
912
"github.com/microsoft/go-sqlcmd/internal/config"
10-
"runtime"
11-
"testing"
13+
"github.com/microsoft/go-sqlcmd/internal/tools"
1214
)
1315

14-
// TestOpen runs a sanity test of `sqlcmd open`
16+
// TestAds runs a sanity test of `sqlcmd open ads`
1517
func TestAds(t *testing.T) {
1618
if runtime.GOOS != "windows" {
17-
t.Skip("Ads support only on Windows at this time")
19+
t.Skip("ADS support only on Windows at this time")
20+
}
21+
22+
tool := tools.NewTool("ads")
23+
if !tool.IsInstalled() {
24+
t.Skip("Azure Data Studio is not installed")
1825
}
1926

2027
cmdparser.TestSetup(t)

cmd/modern/root/open/clipboard.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
package open
5+
6+
import (
7+
"github.com/microsoft/go-sqlcmd/cmd/modern/sqlconfig"
8+
"github.com/microsoft/go-sqlcmd/internal/config"
9+
"github.com/microsoft/go-sqlcmd/internal/localizer"
10+
"github.com/microsoft/go-sqlcmd/internal/output"
11+
"github.com/microsoft/go-sqlcmd/internal/pal"
12+
)
13+
14+
// copyPasswordToClipboard copies the password for the current context to the clipboard
15+
// if the user is using SQL authentication. Returns true if a password was copied.
16+
func copyPasswordToClipboard(user *sqlconfig.User, out *output.Output) bool {
17+
if user == nil || user.AuthenticationType != "basic" {
18+
return false
19+
}
20+
21+
// Get the decrypted password from the current context
22+
_, _, password := config.GetCurrentContextInfo()
23+
24+
if password == "" {
25+
return false
26+
}
27+
28+
err := pal.CopyToClipboard(password)
29+
if err != nil {
30+
// Don't fail the command if clipboard copy fails, just warn the user
31+
out.Warn(localizer.Sprintf("Could not copy password to clipboard: %s", err.Error()))
32+
return false
33+
}
34+
35+
out.Info(localizer.Sprintf("Password copied to clipboard - paste it when prompted"))
36+
return true
37+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
package open
5+
6+
import (
7+
"runtime"
8+
"testing"
9+
10+
"github.com/microsoft/go-sqlcmd/cmd/modern/sqlconfig"
11+
"github.com/microsoft/go-sqlcmd/internal/cmdparser"
12+
)
13+
14+
func TestCopyPasswordToClipboardWithNoUser(t *testing.T) {
15+
if runtime.GOOS == "linux" {
16+
t.Skip("Skipping on Linux due to ADS tool initialization issue in tools factory")
17+
}
18+
19+
cmdparser.TestSetup(t)
20+
21+
result := copyPasswordToClipboard(nil, nil)
22+
if result {
23+
t.Error("Expected false when user is nil")
24+
}
25+
}
26+
27+
func TestCopyPasswordToClipboardWithNonBasicAuth(t *testing.T) {
28+
if runtime.GOOS == "linux" {
29+
t.Skip("Skipping on Linux due to ADS tool initialization issue in tools factory")
30+
}
31+
32+
cmdparser.TestSetup(t)
33+
34+
user := &sqlconfig.User{
35+
AuthenticationType: "windows",
36+
Name: "test-user",
37+
}
38+
39+
result := copyPasswordToClipboard(user, nil)
40+
if result {
41+
t.Error("Expected false when auth type is not 'basic'")
42+
}
43+
}
44+
45+
func TestCopyPasswordToClipboardWithEmptyPassword(t *testing.T) {
46+
user := &sqlconfig.User{
47+
AuthenticationType: "basic",
48+
BasicAuth: &sqlconfig.BasicAuthDetails{
49+
Username: "sa",
50+
PasswordEncryption: "",
51+
Password: "",
52+
},
53+
}
54+
55+
if !userShouldCopyPassword(user) {
56+
t.Error("userShouldCopyPassword should return true for basic auth user")
57+
}
58+
}
59+
60+
func TestCopyPasswordToClipboardLogic(t *testing.T) {
61+
if userShouldCopyPassword(nil) {
62+
t.Error("Should not copy password when user is nil")
63+
}
64+
65+
user := &sqlconfig.User{
66+
AuthenticationType: "integrated",
67+
}
68+
if userShouldCopyPassword(user) {
69+
t.Error("Should not copy password when auth type is not basic")
70+
}
71+
72+
user = &sqlconfig.User{
73+
AuthenticationType: "basic",
74+
BasicAuth: &sqlconfig.BasicAuthDetails{
75+
Username: "sa",
76+
Password: "test",
77+
},
78+
}
79+
if !userShouldCopyPassword(user) {
80+
t.Error("Should copy password when auth type is basic")
81+
}
82+
}
83+
84+
// userShouldCopyPassword is a helper that tests the condition logic
85+
func userShouldCopyPassword(user *sqlconfig.User) bool {
86+
if user == nil || user.AuthenticationType != "basic" {
87+
return false
88+
}
89+
return true
90+
}

cmd/modern/root/open/ssms.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
//go:build windows
2+
3+
// Copyright (c) Microsoft Corporation.
4+
// Licensed under the MIT license.
5+
6+
package open
7+
8+
import (
9+
"fmt"
10+
"strings"
11+
12+
"github.com/microsoft/go-sqlcmd/cmd/modern/sqlconfig"
13+
"github.com/microsoft/go-sqlcmd/internal/cmdparser"
14+
"github.com/microsoft/go-sqlcmd/internal/config"
15+
"github.com/microsoft/go-sqlcmd/internal/container"
16+
"github.com/microsoft/go-sqlcmd/internal/localizer"
17+
"github.com/microsoft/go-sqlcmd/internal/tools"
18+
)
19+
20+
// Ssms implements the `sqlcmd open ssms` command. It opens
21+
// SQL Server Management Studio and connects to the current context using the
22+
// credentials specified in the context.
23+
func (c *Ssms) DefineCommand(...cmdparser.CommandOptions) {
24+
options := cmdparser.CommandOptions{
25+
Use: "ssms",
26+
Short: localizer.Sprintf("Open SQL Server Management Studio and connect to current context"),
27+
Examples: []cmdparser.ExampleOptions{{
28+
Description: localizer.Sprintf("Open SSMS and connect using the current context"),
29+
Steps: []string{"sqlcmd open ssms"}}},
30+
Run: c.run,
31+
}
32+
33+
c.Cmd.DefineCommand(options)
34+
}
35+
36+
// Launch SSMS and connect to the current context
37+
func (c *Ssms) run() {
38+
endpoint, user := config.CurrentContext()
39+
40+
// Check if this is a local container connection
41+
isLocalConnection := isLocalEndpoint(endpoint)
42+
43+
// If the context has a local container, ensure it is running, otherwise bail out
44+
if asset := endpoint.AssetDetails; asset != nil && asset.ContainerDetails != nil {
45+
c.ensureContainerIsRunning(asset.Id)
46+
}
47+
48+
// Launch SSMS with connection parameters
49+
c.launchSsms(endpoint.Address, endpoint.Port, user, isLocalConnection)
50+
}
51+
52+
func (c *Ssms) ensureContainerIsRunning(containerID string) {
53+
output := c.Output()
54+
controller := container.NewController()
55+
if !controller.ContainerRunning(containerID) {
56+
output.FatalWithHintExamples([][]string{
57+
{localizer.Sprintf("To start the container"), localizer.Sprintf("sqlcmd start")},
58+
}, localizer.Sprintf("Container is not running"))
59+
}
60+
}
61+
62+
// launchSsms launches SQL Server Management Studio using the specified server and user credentials.
63+
func (c *Ssms) launchSsms(host string, port int, user *sqlconfig.User, isLocalConnection bool) {
64+
output := c.Output()
65+
66+
// Build server connection string
67+
serverArg := fmt.Sprintf("%s,%d", host, port)
68+
69+
args := []string{
70+
"-S", serverArg,
71+
"-nosplash",
72+
}
73+
74+
// Only add -C (trust server certificate) for local connections with self-signed certs
75+
if isLocalConnection {
76+
args = append(args, "-C")
77+
}
78+
79+
// Use SQL authentication if configured (commonly used for SQL Server containers)
80+
if user != nil && user.AuthenticationType == "basic" && user.BasicAuth != nil {
81+
// Escape double quotes in username (SQL Server allows " in login names)
82+
username := strings.ReplaceAll(user.BasicAuth.Username, `"`, `\"`)
83+
args = append(args, "-U", username)
84+
// Note: -P parameter was removed in SSMS 18+ for security reasons
85+
// Copy password to clipboard so user can paste it in the login dialog
86+
copyPasswordToClipboard(user, output)
87+
}
88+
89+
tool := tools.NewTool("ssms")
90+
if !tool.IsInstalled() {
91+
output.Fatal(tool.HowToInstall())
92+
}
93+
94+
c.displayPreLaunchInfo()
95+
96+
_, err := tool.Run(args)
97+
c.CheckErr(err)
98+
}

0 commit comments

Comments
 (0)