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 @@ -61,18 +61,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., Azure Data Studio, VS 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),
}
}
43 changes: 39 additions & 4 deletions cmd/modern/root/open/ads.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,44 @@ func (c *Ads) DefineCommand(...cmdparser.CommandOptions) {
// specific credential store, e.g. on Windows we use the Windows Credential
// Manager.
func (c *Ads) run() {
output := c.Output()
output.Warn(localizer.Sprintf("Azure Data Studio is being retired. This command will be removed in a future release."))

switch runtime.GOOS {
case "windows":
output.Info(localizer.Sprintf(`Alternatives:

VS Code:
winget install Microsoft.VisualStudioCode
sqlcmd open vscode --install-extension

SSMS:
winget install Microsoft.SQLServerManagementStudio
sqlcmd open ssms
`))
case "darwin":
output.Info(localizer.Sprintf(`Alternatives:

VS Code:
brew install --cask visual-studio-code
sqlcmd open vscode --install-extension
Or download: https://code.visualstudio.com/download
`))
default:
output.Info(localizer.Sprintf(`Alternatives:

VS Code:
snap install code --classic
sqlcmd open vscode --install-extension
Or download: https://code.visualstudio.com/download
`))
}

tool := tools.NewTool("ads")
if !tool.IsInstalled() {
output.Fatal(localizer.Sprintf("Azure Data Studio is not installed."))
}

endpoint, user := config.CurrentContext()

// If the context has a local container, ensure it is running, otherwise bail out
Expand Down Expand Up @@ -66,7 +104,6 @@ func (c *Ads) ensureContainerIsRunning(endpoint sqlconfig.Endpoint) {

// launchAds launches the Azure Data Studio using the specified server and username.
func (c *Ads) launchAds(host string, port int, username string) {
output := c.Output()
args := []string{
"-r",
fmt.Sprintf(
Expand All @@ -89,9 +126,7 @@ func (c *Ads) launchAds(host string, port int, username string) {
}

tool := tools.NewTool("ads")
if !tool.IsInstalled() {
output.Fatal(tool.HowToInstall())
}
tool.IsInstalled() // precondition for Run; already verified in run()

c.displayPreLaunchInfo()

Expand Down
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
10 changes: 7 additions & 3 deletions cmd/modern/root/open/ads_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ type Ads struct {
// Ctrl+C here.
func (c *Ads) displayPreLaunchInfo() {
output := c.Output()

output.Info(localizer.Sprintf("Press Ctrl+C to exit this process..."))
output.Info(localizer.Sprintf("Launching Azure Data Studio..."))
}

// persistCredentialForAds stores a SQL password in the Windows Credential Manager
Expand Down Expand Up @@ -77,7 +76,12 @@ func (c *Ads) adsKey(instance, database, authType, user string) string {
// the same target name as the current instance's credential.
func (c *Ads) removePreviousCredential() {
credentials, err := credman.EnumerateCredentials("", true)
c.CheckErr(err)
if err != nil {
// ERROR_NOT_FOUND (element not found) is expected when no
// credentials exist yet. Any other error is non-fatal here
// since we're only trying to clean up a previous entry.
return
}

for _, cred := range credentials {
if cred.TargetName == c.credential.TargetName {
Expand Down
36 changes: 36 additions & 0 deletions cmd/modern/root/open/clipboard.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// 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 SQL password to the system clipboard.
// The password remains on the clipboard until the user or another application
// clears it; callers rely on the user heeding the "clear your clipboard" message.
func copyPasswordToClipboard(user *sqlconfig.User, out *output.Output) bool {
if user == nil || user.AuthenticationType != "basic" {
return false
}

_, _, password := config.GetCurrentContextInfo()

if password == "" {
return false
}

err := pal.CopyToClipboard(password)
if err != nil {
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, then clear your clipboard"))
return true
}
43 changes: 43 additions & 0 deletions cmd/modern/root/open/clipboard_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// 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'")
}
}
Loading
Loading