Skip to content

Commit 07e30ce

Browse files
authored
add more command line switches for connection string management (#22)
* add more command line switches * better defaults
1 parent 8e1035e commit 07e30ce

File tree

5 files changed

+96
-18
lines changed

5 files changed

+96
-18
lines changed

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,14 @@ We will be implementing command line switches and behaviors over time. Several s
1717
-When prompted, the user can type the password to complete a connection
1818

1919
- `-R` switch will be removed. The go runtime does not provide access to user locale information, and it's not readily available through syscall on all supported platforms.
20+
- `-I` switch will be removed. To disable quoted identifier behavior, add `SET QUOTED IDENTIFIER OFF` in your scripts.
21+
- `-N` now takes a string value that can be one of `true`, `false`, or `disable` to specify the encryption choice. (`default` is the same as omitting the parameter)
22+
- If `-N` and `-C` are not provided, sqlcmd will negotiate authentication with the server without validating the server certificate.
23+
- If `-N` is provided but `-C` is not, sqlcmd will require validation of the server certificate. Note that a `false` value for encryption could still lead to encryption of the login packet.
24+
- If both `-N` and `-C` are provided, sqlcmd will use their values for encryption negotiation.
25+
- More information about client/server encryption negotiation can be found at <https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-tds/60f56408-0188-4cd5-8b90-25c6f2423868>
2026
- Some behaviors that were kept to maintain compatibility with `OSQL` may be changed, such as alignment of column headers for some data types.
21-
- All commands must fit on one line, even `EXIT`. Interactive mode will not check for open parentheses or quotes for commands and prompt for successive lines. The native sqlcmd allows the query run by `EXIT(query)` to span multiple lines.
27+
- All commands must fit on one line, even `EXIT`. Interactive mode will not check for open parentheses or quotes for commands and prompt for successive lines. The ODBC sqlcmd allows the query run by `EXIT(query)` to span multiple lines.
2228

2329
### Azure Active Directory Authentication
2430

cmd/sqlcmd/main.go

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,20 @@ type SQLCmdArguments struct {
3737
UseAad bool `short:"G" xor:"auth" help:"Tells sqlcmd to use Active Directory authentication. If no user name is provided, authentication method ActiveDirectoryDefault is used. If a password is provided, ActiveDirectoryPassword is used. Otherwise ActiveDirectoryInteractive is used."`
3838
DisableVariableSubstitution bool `short:"x" help:"Causes sqlcmd to ignore scripting variables. This parameter is useful when a script contains many INSERT statements that may contain strings that have the same format as regular variables, such as $(variable_name)."`
3939
Variables map[string]string `short:"v" help:"Creates a sqlcmd scripting variable that can be used in a sqlcmd script. Enclose the value in quotation marks if the value contains spaces. You can specify multiple var=values values. If there are errors in any of the values specified, sqlcmd generates an error message and then exits"`
40+
PacketSize int `short:"a" help:"Requests a packet of a different size. This option sets the sqlcmd scripting variable SQLCMDPACKETSIZE. packet_size must be a value between 512 and 32767. The default = 4096. A larger packet size can enhance performance for execution of scripts that have lots of SQL statements between GO commands. You can request a larger packet size. However, if the request is denied, sqlcmd uses the server default for packet size."`
41+
LoginTimeout int `short:"l" default:"-1" help:"Specifies the number of seconds before a sqlcmd login to the go-mssqldb driver times out when you try to connect to a server. This option sets the sqlcmd scripting variable SQLCMDLOGINTIMEOUT. The default value is 30. 0 means infinite."`
42+
WorkstationName string `short:"H" help:"This option sets the sqlcmd scripting variable SQLCMDWORKSTATION. The workstation name is listed in the hostname column of the sys.sysprocesses catalog view and can be returned using the stored procedure sp_who. If this option is not specified, the default is the current computer name. This name can be used to identify different sqlcmd sessions."`
43+
ApplicationIntent string `short:"K" default:"default" enum:"default,ReadOnly" help:"Declares the application workload type when connecting to a server. The only currently supported value is ReadOnly. If -K is not specified, the sqlcmd utility will not support connectivity to a secondary replica in an Always On availability group."`
44+
EncryptConnection string `short:"N" default:"default" enum:"default,false,true,disable" help:"This switch is used by the client to request an encrypted connection."`
45+
}
46+
47+
// Validate accounts for settings not described by Kong attributes
48+
func (a *SQLCmdArguments) Validate() error {
49+
if a.PacketSize != 0 && (a.PacketSize < 512 || a.PacketSize > 32767) {
50+
return fmt.Errorf(`'-a %d': Packet size has to be a number between 512 and 32767.`, a.PacketSize)
51+
}
52+
53+
return nil
4054
}
4155

4256
// newArguments constructs a SQLCmdArguments instance with default values
@@ -87,8 +101,13 @@ func main() {
87101
// setVars initializes scripting variables from command line arguments
88102
func setVars(vars *sqlcmd.Variables, args *SQLCmdArguments) {
89103
varmap := map[string]func(*SQLCmdArguments) string{
90-
sqlcmd.SQLCMDDBNAME: func(a *SQLCmdArguments) string { return a.DatabaseName },
91-
sqlcmd.SQLCMDLOGINTIMEOUT: func(a *SQLCmdArguments) string { return "" },
104+
sqlcmd.SQLCMDDBNAME: func(a *SQLCmdArguments) string { return a.DatabaseName },
105+
sqlcmd.SQLCMDLOGINTIMEOUT: func(a *SQLCmdArguments) string {
106+
if a.LoginTimeout > -1 {
107+
return fmt.Sprint(a.LoginTimeout)
108+
}
109+
return ""
110+
},
92111
sqlcmd.SQLCMDUSEAAD: func(a *SQLCmdArguments) string {
93112
if a.UseAad {
94113
return "true"
@@ -101,10 +120,15 @@ func setVars(vars *sqlcmd.Variables, args *SQLCmdArguments) {
101120
}
102121
return ""
103122
},
104-
sqlcmd.SQLCMDWORKSTATION: func(a *SQLCmdArguments) string { return "" },
105-
sqlcmd.SQLCMDSERVER: func(a *SQLCmdArguments) string { return a.Server },
106-
sqlcmd.SQLCMDERRORLEVEL: func(a *SQLCmdArguments) string { return "" },
107-
sqlcmd.SQLCMDPACKETSIZE: func(a *SQLCmdArguments) string { return "" },
123+
sqlcmd.SQLCMDWORKSTATION: func(a *SQLCmdArguments) string { return args.WorkstationName },
124+
sqlcmd.SQLCMDSERVER: func(a *SQLCmdArguments) string { return a.Server },
125+
sqlcmd.SQLCMDERRORLEVEL: func(a *SQLCmdArguments) string { return "" },
126+
sqlcmd.SQLCMDPACKETSIZE: func(a *SQLCmdArguments) string {
127+
if args.PacketSize > 0 {
128+
return fmt.Sprint(args.PacketSize)
129+
}
130+
return ""
131+
},
108132
sqlcmd.SQLCMDUSER: func(a *SQLCmdArguments) string { return a.UserName },
109133
sqlcmd.SQLCMDSTATTIMEOUT: func(a *SQLCmdArguments) string { return "" },
110134
sqlcmd.SQLCMDHEADERS: func(a *SQLCmdArguments) string { return "" },
@@ -127,6 +151,22 @@ func setVars(vars *sqlcmd.Variables, args *SQLCmdArguments) {
127151

128152
}
129153

154+
func setConnect(s *sqlcmd.Sqlcmd, args *SQLCmdArguments) {
155+
if !args.DisableCmdAndWarn {
156+
s.Connect.Password = os.Getenv(sqlcmd.SQLCMDPASSWORD)
157+
}
158+
s.Connect.UseTrustedConnection = args.UseTrustedConnection
159+
s.Connect.TrustServerCertificate = args.TrustServerCertificate
160+
s.Connect.AuthenticationMethod = args.authenticationMethod(s.Connect.Password != "")
161+
s.Connect.DisableEnvironmentVariables = args.DisableCmdAndWarn
162+
s.Connect.DisableVariableSubstitution = args.DisableVariableSubstitution
163+
s.Connect.ApplicationIntent = args.ApplicationIntent
164+
s.Connect.LoginTimeoutSeconds = args.LoginTimeout
165+
s.Connect.Encrypt = args.EncryptConnection
166+
s.Connect.PacketSize = args.PacketSize
167+
s.Connect.WorkstationName = args.WorkstationName
168+
}
169+
130170
func run(vars *sqlcmd.Variables) (int, error) {
131171
wd, err := os.Getwd()
132172
if err != nil {
@@ -144,9 +184,7 @@ func run(vars *sqlcmd.Variables) (int, error) {
144184
}
145185

146186
s := sqlcmd.New(line, wd, vars)
147-
if !args.DisableCmdAndWarn {
148-
s.Connect.Password = os.Getenv(sqlcmd.SQLCMDPASSWORD)
149-
}
187+
150188
if args.BatchTerminator != "GO" {
151189
err = s.Cmd.SetBatchTerminator(args.BatchTerminator)
152190
if err != nil {
@@ -156,11 +194,8 @@ func run(vars *sqlcmd.Variables) (int, error) {
156194
if err != nil {
157195
return 1, err
158196
}
159-
s.Connect.UseTrustedConnection = args.UseTrustedConnection
160-
s.Connect.TrustServerCertificate = args.TrustServerCertificate
161-
s.Connect.AuthenticationMethod = args.authenticationMethod(s.Connect.Password != "")
162-
s.Connect.DisableEnvironmentVariables = args.DisableCmdAndWarn
163-
s.Connect.DisableVariableSubstitution = args.DisableVariableSubstitution
197+
198+
setConnect(s, &args)
164199
s.Format = sqlcmd.NewSQLCmdDefaultFormatter(false)
165200
if args.OutputFile != "" {
166201
err = s.RunCommand(s.Cmd["OUT"], []string{args.OutputFile})

cmd/sqlcmd/main_test.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,10 @@ func TestValidCommandLineToArgsConversion(t *testing.T) {
6060
}},
6161
// Notice no "" around the value with a space in it. It seems quotes get stripped out somewhere before Parse when invoking on a real command line
6262
{[]string{"-v", "x=y", "-v", `y=a space`}, func(args SQLCmdArguments) bool {
63-
return args.Variables["x"] == "y" && args.Variables["y"] == "a space"
63+
return args.LoginTimeout == -1 && args.Variables["x"] == "y" && args.Variables["y"] == "a space"
64+
}},
65+
{[]string{"-a", "550", "-l", "45", "-H", "mystation", "-K", "ReadOnly", "-N", "true"}, func(args SQLCmdArguments) bool {
66+
return args.PacketSize == 550 && args.LoginTimeout == 45 && args.WorkstationName == "mystation" && args.ApplicationIntent == "ReadOnly" && args.EncryptConnection == "true"
6467
}},
6568
}
6669

@@ -86,6 +89,8 @@ func TestInvalidCommandLine(t *testing.T) {
8689

8790
commands := []cmdLineTest{
8891
{[]string{"-E", "-U", "someuser"}, "--use-trusted-connection and --user-name can't be used together"},
92+
// the test prefix is a kong artifact https://github.com/alecthomas/kong/issues/221
93+
{[]string{"-a", "100"}, "test: '-a 100': Packet size has to be a number between 512 and 32767."},
8994
}
9095

9196
for _, test := range commands {

pkg/sqlcmd/sqlcmd.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,16 @@ type ConnectSettings struct {
4545
DisableVariableSubstitution bool
4646
// Password is the password used with SQL authentication
4747
Password string
48+
// Encrypt is the choice of encryption
49+
Encrypt string
50+
// PacketSize is the size of the packet for TDS communication
51+
PacketSize int
52+
// LoginTimeoutSeconds specifies the timeout for establishing a connection
53+
LoginTimeoutSeconds int
54+
// WorkstationName is the string to use to identify the host in server DMVs
55+
WorkstationName string
56+
// ApplicationIntent can only be empty or "ReadOnly"
57+
ApplicationIntent string
4858
}
4959

5060
func (c ConnectSettings) authenticationMethod() string {
@@ -233,6 +243,21 @@ func (s *Sqlcmd) ConnectionString() (connectionString string, err error) {
233243
if s.Connect.TrustServerCertificate {
234244
query.Add("trustservercertificate", "true")
235245
}
246+
if s.Connect.ApplicationIntent != "" && s.Connect.ApplicationIntent != "default" {
247+
query.Add("applicationintent", s.Connect.ApplicationIntent)
248+
}
249+
if s.Connect.LoginTimeoutSeconds > 0 {
250+
query.Add("connection timeout", fmt.Sprint(s.Connect.LoginTimeoutSeconds))
251+
}
252+
if s.Connect.PacketSize > 0 {
253+
query.Add("packet size", fmt.Sprint(s.Connect.PacketSize))
254+
}
255+
if s.Connect.WorkstationName != "" {
256+
query.Add("workstation id", s.Connect.WorkstationName)
257+
}
258+
if s.Connect.Encrypt != "" && s.Connect.Encrypt != "default" {
259+
query.Add("encrypt", s.Connect.Encrypt)
260+
}
236261
connectionURL.RawQuery = query.Encode()
237262
return connectionURL.String(), nil
238263
}

pkg/sqlcmd/sqlcmd_test.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,18 @@ func TestConnectionStringFromSqlCmd(t *testing.T) {
2929

3030
{nil, nil, "sqlserver://."},
3131
{
32-
&ConnectSettings{TrustServerCertificate: true},
32+
&ConnectSettings{TrustServerCertificate: true, WorkstationName: "mystation"},
3333
func(vars *Variables) {
3434
vars.Set(SQLCMDDBNAME, "somedatabase")
3535
},
36-
"sqlserver://.?database=somedatabase&trustservercertificate=true",
36+
"sqlserver://.?database=somedatabase&trustservercertificate=true&workstation+id=mystation",
37+
},
38+
{
39+
&ConnectSettings{WorkstationName: "mystation", Encrypt: "false"},
40+
func(vars *Variables) {
41+
vars.Set(SQLCMDDBNAME, "somedatabase")
42+
},
43+
"sqlserver://.?database=somedatabase&encrypt=false&workstation+id=mystation",
3744
},
3845
{
3946
&ConnectSettings{TrustServerCertificate: true, Password: pwd},

0 commit comments

Comments
 (0)