Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ linux-s390x/sqlcmd
# Build artifacts in root
/sqlcmd
/sqlcmd_binary
/modern

# certificates used for local testing
*.der
Expand Down
39 changes: 36 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,18 +59,51 @@ The Homebrew package manager may be used on Linux and Windows Subsystem for Linu

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

### Create SQL Server instance using local container runtime and connect using Azure Data Studio
### Create SQL Server instance using local container runtime

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

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

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

### Connect using Visual Studio Code

Use `sqlcmd open vscode` to open Visual Studio Code with a connection profile configured for the current context:

```
sqlcmd open vscode
```

This command will:
1. **Create a connection profile** in VS Code's user settings with the current context name
2. **Copy the password to clipboard** so you can paste it when prompted
3. **Launch VS Code** ready to connect

To also install the MSSQL extension (if not already installed), add the `--install-extension` flag:

```
sqlcmd open vscode --install-extension
```

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.

### Connect using SQL Server Management Studio (Windows)

On Windows, use `sqlcmd open ssms` to open SQL Server Management Studio pre-configured to connect to the current context:

```
sqlcmd open ssms
```

This command will:
1. **Copy the password to clipboard** so you can paste it in the login dialog
2. **Launch SSMS** with the server and username pre-filled
3. You'll be prompted for the password - just paste from clipboard (Ctrl+V)

### The ~/.sqlcmd/sqlconfig file

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`.
Expand Down
6 changes: 4 additions & 2 deletions cmd/modern/root/open.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,21 @@ type Open struct {
func (c *Open) DefineCommand(...cmdparser.CommandOptions) {
options := cmdparser.CommandOptions{
Use: "open",
Short: localizer.Sprintf("Open tools (e.g Azure Data Studio) for current context"),
Short: localizer.Sprintf("Open tools (e.g., Visual Studio Code, SSMS) for current context"),
SubCommands: c.SubCommands(),
}

c.Cmd.DefineCommand(options)
}

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

return []cmdparser.Command{
cmdparser.New[*open.Ads](dependencies),
cmdparser.New[*open.VSCode](dependencies),
cmdparser.New[*open.Ssms](dependencies),
}
}
15 changes: 11 additions & 4 deletions cmd/modern/root/open/ads_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,24 @@
package open

import (
"runtime"
"testing"

"github.com/microsoft/go-sqlcmd/cmd/modern/sqlconfig"
"github.com/microsoft/go-sqlcmd/internal/cmdparser"
"github.com/microsoft/go-sqlcmd/internal/config"
"runtime"
"testing"
"github.com/microsoft/go-sqlcmd/internal/tools"
)

// TestOpen runs a sanity test of `sqlcmd open`
// TestAds runs a sanity test of `sqlcmd open ads`
func TestAds(t *testing.T) {
if runtime.GOOS != "windows" {
t.Skip("Ads support only on Windows at this time")
t.Skip("ADS support only on Windows at this time")
}

tool := tools.NewTool("ads")
if !tool.IsInstalled() {
t.Skip("Azure Data Studio is not installed")
}

cmdparser.TestSetup(t)
Expand Down
37 changes: 37 additions & 0 deletions cmd/modern/root/open/clipboard.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

package open

import (
"github.com/microsoft/go-sqlcmd/cmd/modern/sqlconfig"
"github.com/microsoft/go-sqlcmd/internal/config"
"github.com/microsoft/go-sqlcmd/internal/localizer"
"github.com/microsoft/go-sqlcmd/internal/output"
"github.com/microsoft/go-sqlcmd/internal/pal"
)

// copyPasswordToClipboard copies the password for the current context to the clipboard
// if the user is using SQL authentication. Returns true if a password was copied.
func copyPasswordToClipboard(user *sqlconfig.User, out *output.Output) bool {
if user == nil || user.AuthenticationType != "basic" {
return false
}

// Get the decrypted password from the current context
_, _, password := config.GetCurrentContextInfo()

if password == "" {
return false
}

err := pal.CopyToClipboard(password)
if err != nil {
// Don't fail the command if clipboard copy fails, just warn the user
out.Warn(localizer.Sprintf("Could not copy password to clipboard: %s", err.Error()))
return false
}

out.Info(localizer.Sprintf("Password copied to clipboard - paste it when prompted"))
return true
}
90 changes: 90 additions & 0 deletions cmd/modern/root/open/clipboard_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

package open

import (
"runtime"
"testing"

"github.com/microsoft/go-sqlcmd/cmd/modern/sqlconfig"
"github.com/microsoft/go-sqlcmd/internal/cmdparser"
)

func TestCopyPasswordToClipboardWithNoUser(t *testing.T) {
if runtime.GOOS == "linux" {
t.Skip("Skipping on Linux due to ADS tool initialization issue in tools factory")
}

cmdparser.TestSetup(t)

result := copyPasswordToClipboard(nil, nil)
if result {
t.Error("Expected false when user is nil")
}
}

func TestCopyPasswordToClipboardWithNonBasicAuth(t *testing.T) {
if runtime.GOOS == "linux" {
t.Skip("Skipping on Linux due to ADS tool initialization issue in tools factory")
}

cmdparser.TestSetup(t)

user := &sqlconfig.User{
AuthenticationType: "windows",
Name: "test-user",
}

result := copyPasswordToClipboard(user, nil)
if result {
t.Error("Expected false when auth type is not 'basic'")
}
}

func TestCopyPasswordToClipboardWithEmptyPassword(t *testing.T) {
user := &sqlconfig.User{
AuthenticationType: "basic",
BasicAuth: &sqlconfig.BasicAuthDetails{
Username: "sa",
PasswordEncryption: "",
Password: "",
},
}

if !userShouldCopyPassword(user) {
t.Error("userShouldCopyPassword should return true for basic auth user")
}
}

func TestCopyPasswordToClipboardLogic(t *testing.T) {
if userShouldCopyPassword(nil) {
t.Error("Should not copy password when user is nil")
}

user := &sqlconfig.User{
AuthenticationType: "integrated",
}
if userShouldCopyPassword(user) {
t.Error("Should not copy password when auth type is not basic")
}

user = &sqlconfig.User{
AuthenticationType: "basic",
BasicAuth: &sqlconfig.BasicAuthDetails{
Username: "sa",
Password: "test",
},
}
if !userShouldCopyPassword(user) {
t.Error("Should copy password when auth type is basic")
}
}

// userShouldCopyPassword is a helper that tests the condition logic
func userShouldCopyPassword(user *sqlconfig.User) bool {
if user == nil || user.AuthenticationType != "basic" {
return false
}
return true
}
98 changes: 98 additions & 0 deletions cmd/modern/root/open/ssms.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
//go:build windows

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

package open

import (
"fmt"
"strings"

"github.com/microsoft/go-sqlcmd/cmd/modern/sqlconfig"
"github.com/microsoft/go-sqlcmd/internal/cmdparser"
"github.com/microsoft/go-sqlcmd/internal/config"
"github.com/microsoft/go-sqlcmd/internal/container"
"github.com/microsoft/go-sqlcmd/internal/localizer"
"github.com/microsoft/go-sqlcmd/internal/tools"
)

// Ssms implements the `sqlcmd open ssms` command. It opens
// SQL Server Management Studio and connects to the current context using the
// credentials specified in the context.
func (c *Ssms) DefineCommand(...cmdparser.CommandOptions) {
options := cmdparser.CommandOptions{
Use: "ssms",
Short: localizer.Sprintf("Open SQL Server Management Studio and connect to current context"),
Examples: []cmdparser.ExampleOptions{{
Description: localizer.Sprintf("Open SSMS and connect using the current context"),
Steps: []string{"sqlcmd open ssms"}}},
Run: c.run,
}

c.Cmd.DefineCommand(options)
}

// Launch SSMS and connect to the current context
func (c *Ssms) run() {
endpoint, user := config.CurrentContext()

// Check if this is a local container connection
isLocalConnection := isLocalEndpoint(endpoint)

// If the context has a local container, ensure it is running, otherwise bail out
if asset := endpoint.AssetDetails; asset != nil && asset.ContainerDetails != nil {
c.ensureContainerIsRunning(asset.Id)
}

// Launch SSMS with connection parameters
c.launchSsms(endpoint.Address, endpoint.Port, user, isLocalConnection)
}

func (c *Ssms) ensureContainerIsRunning(containerID string) {
output := c.Output()
controller := container.NewController()
if !controller.ContainerRunning(containerID) {
output.FatalWithHintExamples([][]string{
{localizer.Sprintf("To start the container"), localizer.Sprintf("sqlcmd start")},
}, localizer.Sprintf("Container is not running"))
}
}

// launchSsms launches SQL Server Management Studio using the specified server and user credentials.
func (c *Ssms) launchSsms(host string, port int, user *sqlconfig.User, isLocalConnection bool) {
output := c.Output()

// Build server connection string
serverArg := fmt.Sprintf("%s,%d", host, port)

args := []string{
"-S", serverArg,
"-nosplash",
}

// Only add -C (trust server certificate) for local connections with self-signed certs
if isLocalConnection {
args = append(args, "-C")
}

// Use SQL authentication if configured (commonly used for SQL Server containers)
if user != nil && user.AuthenticationType == "basic" && user.BasicAuth != nil {
// Escape double quotes in username (SQL Server allows " in login names)
username := strings.ReplaceAll(user.BasicAuth.Username, `"`, `\"`)
args = append(args, "-U", username)
// Note: -P parameter was removed in SSMS 18+ for security reasons
// Copy password to clipboard so user can paste it in the login dialog
copyPasswordToClipboard(user, output)
}

tool := tools.NewTool("ssms")
if !tool.IsInstalled() {
output.Fatal(tool.HowToInstall())
}

c.displayPreLaunchInfo()

_, err := tool.Run(args)
c.CheckErr(err)
}
Loading
Loading