Skip to content

Commit 079a9ca

Browse files
committed
Add sqlcmd use
1 parent 08e5588 commit 079a9ca

File tree

7 files changed

+417
-184
lines changed

7 files changed

+417
-184
lines changed

cmd/modern/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ func (c *Root) SubCommands() []cmdparser.Command {
6868
cmdparser.New[*root.Start](dependencies),
6969
cmdparser.New[*root.Stop](dependencies),
7070
cmdparser.New[*root.Uninstall](dependencies),
71+
cmdparser.New[*root.Use](dependencies),
7172
}
7273

7374
// BUG(stuartpa): - Add Linux support

cmd/modern/root/install/mssql-base.go

Lines changed: 23 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@ import (
1111
"github.com/microsoft/go-sqlcmd/internal/tools"
1212
"net/url"
1313
"os"
14-
"path"
1514
"path/filepath"
1615
"runtime"
1716
"strings"
1817

18+
"github.com/microsoft/go-sqlcmd/pkg/mssqlcontainer"
19+
1920
"github.com/microsoft/go-sqlcmd/cmd/modern/root/open"
2021

2122
"github.com/microsoft/go-sqlcmd/cmd/modern/sqlconfig"
@@ -224,7 +225,7 @@ func (c *MssqlBase) AddFlags(
224225
String: &c.usingDatabaseUrl,
225226
DefaultString: "",
226227
Name: "using",
227-
Usage: "Download (into container) and attach database (.bak) from URL",
228+
Usage: "Download and use database from .bak/.bacpac/.mdf/.7z URL",
228229
})
229230

230231
addFlag(cmdparser.FlagOptions{
@@ -514,18 +515,16 @@ func (c *MssqlBase) validateUsingUrlExists() {
514515
if u.Path == "" {
515516
output.FatalfWithHints(
516517
[]string{
517-
"--using URL must have a path to .bak or .bacpac file",
518+
"--using URL must have a path to .bak, .bacpac or .mdf (.7z) file",
518519
},
519520
"%q is not a valid URL for --using flag", c.usingDatabaseUrl)
520521
}
521522

522-
// At the moment we only support attaching .bak files, but we should
523-
// support .bacpacs and .mdfs in the future
524523
_, f := filepath.Split(u.Path)
525-
if filepath.Ext(f) != ".bak" && filepath.Ext(f) != ".bacpac" {
524+
if filepath.Ext(f) != ".bak" && filepath.Ext(f) != ".bacpac" && filepath.Ext(f) != ".mdf" && filepath.Ext(f) != ".7z" {
526525
output.FatalfWithHints(
527526
[]string{
528-
"--using file URL must be a .bak or .bacpac file",
527+
"--using file URL must be a .bak, .bacpac, or .mdf (.7z) file",
529528
},
530529
"Invalid --using file type, extension %q is not supported", filepath.Ext(f))
531530
}
@@ -582,47 +581,22 @@ CHECK_POLICY=OFF`
582581
}
583582
}
584583

585-
func getDbNameAsIdentifier(dbName string) string {
586-
escapedDbNAme := strings.ReplaceAll(dbName, "'", "''")
587-
return strings.ReplaceAll(escapedDbNAme, "]", "]]")
588-
}
589-
590-
func getDbNameAsNonIdentifier(dbName string) string {
591-
return strings.ReplaceAll(dbName, "]", "]]")
592-
}
593-
594-
// parseDbName returns the databaseName from --using arg
595-
// It sets database name to the specified database name
596-
// or in absence of it, it is set to the filename without
597-
// extension.
598-
func parseDbName(usingDbUrl string) string {
599-
u, _ := url.Parse(usingDbUrl)
600-
dbToken := path.Base(u.Path)
601-
if dbToken != "." && dbToken != "/" {
602-
lastIdx := strings.LastIndex(dbToken, ".bak")
603-
if lastIdx == -1 {
604-
lastIdx = strings.LastIndex(dbToken, ".bacpac")
605-
}
606-
if lastIdx != -1 {
607-
//Get file name without extension
608-
fileName := dbToken[0:lastIdx]
609-
lastIdx += 5
610-
if lastIdx >= len(dbToken) {
611-
return fileName
612-
}
613-
//Return database name if it was specified
614-
return dbToken[lastIdx:]
615-
}
616-
}
617-
return ""
618-
}
619-
620584
func extractUrl(usingArg string) string {
621585
urlEndIdx := strings.LastIndex(usingArg, ".bak")
586+
if urlEndIdx == -1 {
587+
urlEndIdx = strings.LastIndex(usingArg, ".mdf")
588+
}
622589
if urlEndIdx != -1 {
623590
return usingArg[0:(urlEndIdx + 4)]
624591
}
625592

593+
if urlEndIdx == -1 {
594+
urlEndIdx = strings.LastIndex(usingArg, ".7z")
595+
if urlEndIdx != -1 {
596+
return usingArg[0:(urlEndIdx + 3)]
597+
}
598+
}
599+
626600
if urlEndIdx == -1 {
627601
urlEndIdx = strings.LastIndex(usingArg, ".bacpac")
628602
if urlEndIdx != -1 {
@@ -639,111 +613,15 @@ func (c *MssqlBase) downloadAndRestoreDb(
639613
userName string,
640614
password string,
641615
) {
642-
output := c.Cmd.Output()
643-
databaseName := parseDbName(c.usingDatabaseUrl)
644-
databaseUrl := extractUrl(c.usingDatabaseUrl)
645-
646-
_, file := filepath.Split(databaseUrl)
647-
648-
// Download file from URL into container
649-
output.Infof("Downloading %s", file)
650-
651-
temporaryFolder := "/var/opt/mssql/backup"
652-
653-
controller.DownloadFile(
616+
mssqlcontainer.DownloadAndRestoreDb(
617+
controller,
654618
containerId,
655-
databaseUrl,
656-
temporaryFolder,
657-
)
658-
659-
dbNameAsIdentifier := getDbNameAsIdentifier(databaseName)
660-
dbNameAsNonIdentifier := getDbNameAsNonIdentifier(databaseName)
661-
662-
u, err := url.Parse(databaseUrl)
663-
c.CheckErr(err)
664-
665-
_, f := filepath.Split(u.Path)
666-
if filepath.Ext(f) == ".bak" {
667-
// Restore database from file
668-
output.Infof("Restoring database %s", databaseName)
669-
670-
text := `SET NOCOUNT ON;
671-
672-
-- Build a SQL Statement to restore any .bak file to the Linux filesystem
673-
DECLARE @sql NVARCHAR(max)
674-
675-
-- This table definition works since SQL Server 2017, therefore
676-
-- works for all SQL Server containers (which started in 2017)
677-
DECLARE @fileListTable TABLE (
678-
[LogicalName] NVARCHAR(128),
679-
[PhysicalName] NVARCHAR(260),
680-
[Type] CHAR(1),
681-
[FileGroupName] NVARCHAR(128),
682-
[Size] NUMERIC(20,0),
683-
[MaxSize] NUMERIC(20,0),
684-
[FileID] BIGINT,
685-
[CreateLSN] NUMERIC(25,0),
686-
[DropLSN] NUMERIC(25,0),
687-
[UniqueID] UNIQUEIDENTIFIER,
688-
[ReadOnlyLSN] NUMERIC(25,0),
689-
[ReadWriteLSN] NUMERIC(25,0),
690-
[BackupSizeInBytes] BIGINT,
691-
[SourceBlockSize] INT,
692-
[FileGroupID] INT,
693-
[LogGroupGUID] UNIQUEIDENTIFIER,
694-
[DifferentialBaseLSN] NUMERIC(25,0),
695-
[DifferentialBaseGUID] UNIQUEIDENTIFIER,
696-
[IsReadOnly] BIT,
697-
[IsPresent] BIT,
698-
[TDEThumbprint] VARBINARY(32),
699-
[SnapshotURL] NVARCHAR(360)
700-
)
701-
702-
INSERT INTO @fileListTable
703-
EXEC('RESTORE FILELISTONLY FROM DISK = ''%s/%s''')
704-
SET @sql = 'RESTORE DATABASE [%s] FROM DISK = ''%s/%s'' WITH '
705-
SELECT @sql = @sql + char(13) + ' MOVE ''' + LogicalName + ''' TO ''/var/opt/mssql/' + LogicalName + '.' + RIGHT(PhysicalName,CHARINDEX('\',PhysicalName)) + ''','
706-
FROM @fileListTable
707-
WHERE IsPresent = 1
708-
SET @sql = SUBSTRING(@sql, 1, LEN(@sql)-1)
709-
EXEC(@sql)`
710-
711-
c.query(fmt.Sprintf(text, temporaryFolder, file, dbNameAsIdentifier, temporaryFolder, file))
712-
} else if filepath.Ext(f) == ".bacpac" {
713-
// sqlpackage /a:import /SourceFile:DaasMM.bacpac
714-
// /tu:"joey"
715-
// /tp:"6%oO3L#X%d%8hAH5E*y6plD6#IV1$PEESS$ZAhC8uL#@Q@40e3"
716-
// /tsn:"localhost,1433"
717-
// /tdn:"db1"
718-
719-
controller.DownloadFile(
720-
containerId,
721-
"https://aka.ms/sqlpackage-linux",
722-
"/tmp",
723-
)
724-
725-
controller.RunCmdInContainer(containerId, []string{"apt-get", "update"})
726-
controller.RunCmdInContainer(containerId, []string{"apt-get", "install", "-y", "unzip"})
727-
controller.RunCmdInContainer(containerId, []string{"unzip", "/tmp/sqlpackage-linux", "-d", "/opt/sqlpackage"})
728-
controller.RunCmdInContainer(containerId, []string{"chmod", "+x", "/opt/sqlpackage/sqlpackage"})
729-
//controller.RunCmdInContainer(containerId, []string{"echo", `'export PATH="$PATH:/opt/sqlpackage"' >> ~/.bash_profile`})
730-
//controller.RunCmdInContainer(containerId, []string{"source", "~/.bash_profile"})
731-
controller.RunCmdInContainer(containerId, []string{
732-
"/opt/sqlpackage/sqlpackage",
733-
"/a:import",
734-
"/SourceFile:" + temporaryFolder + "/" + file,
735-
"/tu:" + userName,
736-
"/tp:" + password,
737-
"/tsn:127.0.0.1,1433",
738-
"/tdn:" + dbNameAsIdentifier,
739-
"/TargetTrustServerCertificate:true"})
740-
}
741-
742-
alterDefaultDb := fmt.Sprintf(
743-
"ALTER LOGIN [%s] WITH DEFAULT_DATABASE = [%s]",
619+
c.usingDatabaseUrl,
744620
userName,
745-
dbNameAsNonIdentifier)
746-
c.query(alterDefaultDb)
621+
password,
622+
c.query,
623+
c.Cmd.Output(),
624+
)
747625
}
748626

749627
func (c *MssqlBase) downloadImage(

cmd/modern/root/install/mssql-base_test.go

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6,45 +6,6 @@ import (
66
"github.com/stretchr/testify/assert"
77
)
88

9-
func TestGetDbNameIfExists(t *testing.T) {
10-
11-
type test struct {
12-
input string
13-
expectedIdentifierOp string
14-
expectedNonIdentifierOp string
15-
}
16-
17-
tests := []test{
18-
// Positive Testcases
19-
// Database name specified
20-
{"https://example.com/my%20random%20bac%27kp%5Dack.bak,myDbName", "myDbName", "myDbName"},
21-
{"https://example.com/my%20random%20bac%27kp%5Dack.bak,myDb Name", "myDb Name", "myDb Name"},
22-
{"https://example.com/my%20random%20bac%27kp%5Dack.bak,myDb Na,me", "myDb Na,me", "myDb Na,me"},
23-
{"https://example.com/my%20random%20bac%27kp%5Dack.bak,[myDb Na,me]", "[myDb Na,me]]", "[myDb Na,me]]"},
24-
{"https://example.com/my%20random%20bac%27kp%5Dack.bak,[myDb Na'me]", "[myDb Na''me]]", "[myDb Na'me]]"},
25-
{"https://example.com/my%20random%20bac%27kp%5Dack.bak,[myDb ,Nam,e]", "[myDb ,Nam,e]]", "[myDb ,Nam,e]]"},
26-
27-
// Delimiter between filename and databaseName is part of the filename
28-
// Decoded filename: my random .bak bac'kp]ack.bak
29-
{"https://example.com/my%20random%20.bak%20bac%27kp%5Dack.bak,[myDb ,Nam,e]", "[myDb ,Nam,e]]", "[myDb ,Nam,e]]"},
30-
31-
// Database name not specified
32-
{"https://example.com/my%20random%20.bak%20bac%27kp%5Dack.bak", "my random .bak bac''kp]]ack", "my random .bak bac'kp]]ack"},
33-
{"https://example.com/my%20random%20.bak%20bac%27kp%5Dack.bak,", "my random .bak bac''kp]]ack", "my random .bak bac'kp]]ack"},
34-
35-
//Negative Testcases
36-
{"https://example.com,myDbName", "", ""},
37-
}
38-
39-
for _, testcase := range tests {
40-
dbname := parseDbName(testcase.input)
41-
dbnameAsIdentifier := getDbNameAsIdentifier(dbname)
42-
dbnameAsNonIdentifier := getDbNameAsNonIdentifier(dbname)
43-
assert.Equal(t, testcase.expectedIdentifierOp, dbnameAsIdentifier, "Unexpected database name as identifier")
44-
assert.Equal(t, testcase.expectedNonIdentifierOp, dbnameAsNonIdentifier, "Unexpected database name as non-identifier")
45-
}
46-
}
47-
489
func TestExtractUrl(t *testing.T) {
4910
type test struct {
5011
inputURL string

cmd/modern/root/use.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
package root
5+
6+
import (
7+
"github.com/microsoft/go-sqlcmd/internal/cmdparser"
8+
"github.com/microsoft/go-sqlcmd/internal/config"
9+
"github.com/microsoft/go-sqlcmd/internal/container"
10+
"github.com/microsoft/go-sqlcmd/internal/sql"
11+
"github.com/microsoft/go-sqlcmd/pkg/mssqlcontainer"
12+
)
13+
14+
type Use struct {
15+
cmdparser.Cmd
16+
17+
url string
18+
19+
sql sql.Sql
20+
}
21+
22+
func (c *Use) DefineCommand(...cmdparser.CommandOptions) {
23+
options := cmdparser.CommandOptions{
24+
Use: "use",
25+
Short: "Download (into container) and use database",
26+
Examples: []cmdparser.ExampleOptions{
27+
{
28+
Description: "Download AdventureWorksLT into container for current context, set as default database",
29+
Steps: []string{`sqlcmd use https://aka.ms/AdventureWorksLT.bak`}},
30+
},
31+
Run: c.run,
32+
FirstArgAlternativeForFlag: &cmdparser.AlternativeForFlagOptions{Flag: "url", Value: &c.url},
33+
}
34+
35+
c.Cmd.DefineCommand(options)
36+
37+
c.AddFlag(cmdparser.FlagOptions{
38+
String: &c.url,
39+
Name: "url",
40+
Usage: "Name of context to set as current context"})
41+
}
42+
43+
func (c *Use) run() {
44+
output := c.Output()
45+
46+
if config.CurrentContextName() == "" {
47+
output.FatalfWithHintExamples([][]string{
48+
{"To view available contexts", "sqlcmd config get-contexts"},
49+
}, "No current context")
50+
}
51+
if config.CurrentContextEndpointHasContainer() {
52+
controller := container.NewController()
53+
id := config.ContainerId()
54+
55+
if !controller.ContainerRunning(id) {
56+
output.FatalfWithHintExamples([][]string{
57+
{"Start container for current context", "sqlcmd start"},
58+
}, "Container for current context is not running")
59+
}
60+
61+
endpoint, user := config.CurrentContext()
62+
63+
c.sql = sql.New(sql.SqlOptions{UnitTesting: false})
64+
c.sql.Connect(endpoint, user, sql.ConnectOptions{Interactive: false})
65+
66+
mssqlcontainer.DownloadAndRestoreDb(
67+
controller,
68+
id,
69+
c.url,
70+
user.BasicAuth.Username,
71+
user.BasicAuth.Password,
72+
c.query,
73+
c.Cmd.Output(),
74+
)
75+
76+
} else {
77+
output.FatalfWithHintExamples([][]string{
78+
{"Create new context with a sql container ", "sqlcmd create mssql"},
79+
}, "Current context does not have a container")
80+
}
81+
}
82+
83+
func (c *Use) query(commandText string) {
84+
c.sql.Query(commandText)
85+
}

internal/output/output.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,11 @@ func (o Output) maskSecrets(text string) string {
214214
// Mask password from T/SQL e.g. ALTER LOGIN [sa] WITH PASSWORD = N'foo';
215215
r := regexp.MustCompile(`(PASSWORD.*\s?=.*\s?N?')(.*)(')`)
216216
text = r.ReplaceAllString(text, "$1********$3")
217+
218+
// Mask password from sqlpackage.exe command line e.g. /TargetPassword:foo
219+
r = regexp.MustCompile(`(/TargetPassword:)(.*)( )`)
220+
text = r.ReplaceAllString(text, "$1********$3")
221+
217222
return text
218223
}
219224

0 commit comments

Comments
 (0)