Skip to content

Commit d4d220b

Browse files
feat: allow -q (initial query) to work with -i (input files)
Previously -q and -i were mutually exclusive. Now -q runs first, then -i files. The -Q flag remains mutually exclusive with -i since -Q exits after executing. Test uses SET NOCOUNT ON (locale-independent) instead of SET LANGUAGE German.
1 parent 56b1fb1 commit d4d220b

File tree

4 files changed

+58
-7
lines changed

4 files changed

+58
-7
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,12 @@ client_interface_name go-mssqldb
163163
program_name sqlcmd
164164
```
165165

166+
- The `-q` (initial query) flag can now be combined with `-i` (input files). The initial query runs first, then the input files are processed. This is useful for setting session options before running scripts:
167+
168+
```bash
169+
sqlcmd -S server -q "SET PARSEONLY ON" -i script.sql
170+
```
171+
166172
- `sqlcmd` supports shared memory and named pipe transport. Use the appropriate protocol prefix on the server name to force a protocol:
167173
* `lpc` for shared memory, only for a localhost. `sqlcmd -S lpc:.`
168174
* `np` for named pipes. Or use the UNC named pipe path as the server name: `sqlcmd -S \\myserver\pipe\sql\query`

cmd/sqlcmd/sqlcmd.go

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,8 @@ func (a *SQLCmdArguments) Validate(c *cobra.Command) (err error) {
148148
}
149149
if err == nil {
150150
switch {
151-
case len(a.InputFile) > 0 && (len(a.Query) > 0 || len(a.InitialQuery) > 0):
152-
err = mutuallyExclusiveError("i", `-Q/-q`)
151+
case len(a.InputFile) > 0 && len(a.Query) > 0:
152+
err = mutuallyExclusiveError("-i", "-Q")
153153
case a.UseTrustedConnection && (len(a.UserName) > 0 || len(a.Password) > 0):
154154
err = mutuallyExclusiveError("-E", `-U/-P`)
155155
case a.UseAad && len(a.AuthenticationMethod) > 0:
@@ -400,7 +400,7 @@ func setFlags(rootCmd *cobra.Command, args *SQLCmdArguments) {
400400
rootCmd.Flags().BoolVarP(&args.Help, "help", "?", false, localizer.Sprintf("-? shows this syntax summary, %s shows modern sqlcmd sub-command help", localizer.HelpFlag))
401401
rootCmd.Flags().StringVar(&args.TraceFile, "trace-file", "", localizer.Sprintf("Write runtime trace to the specified file. Only for advanced debugging."))
402402
var inputfiles []string
403-
rootCmd.Flags().StringSliceVarP(&args.InputFile, "input-file", "i", inputfiles, localizer.Sprintf("Identifies one or more files that contain batches of SQL statements. If one or more files do not exist, sqlcmd will exit. Mutually exclusive with %s/%s", localizer.QueryAndExitFlag, localizer.QueryFlag))
403+
rootCmd.Flags().StringSliceVarP(&args.InputFile, "input-file", "i", inputfiles, localizer.Sprintf("Identifies one or more files that contain batches of SQL statements. If one or more files do not exist, sqlcmd will exit. Mutually exclusive with %s. Can be combined with %s to run an initial query before the input files", localizer.QueryAndExitFlag, localizer.QueryFlag))
404404
rootCmd.Flags().StringVarP(&args.OutputFile, "output-file", "o", "", localizer.Sprintf("Identifies the file that receives output from sqlcmd"))
405405
rootCmd.Flags().BoolVarP(&args.Version, "version", "", false, localizer.Sprintf("Print version information and exit"))
406406
rootCmd.Flags().BoolVarP(&args.TrustServerCertificate, "trust-server-certificate", "C", false, localizer.Sprintf("Implicitly trust the server certificate without validation"))
@@ -890,21 +890,32 @@ func run(vars *sqlcmd.Variables, args *SQLCmdArguments) (int, error) {
890890
s.Query = args.Query
891891
} else if args.InitialQuery != "" {
892892
s.Query = args.InitialQuery
893+
if !isInteractive {
894+
once = true
895+
}
893896
}
894-
iactive := args.InputFile == nil && args.Query == ""
895-
if iactive || s.Query != "" {
897+
898+
// Run initial query (-q) if provided, even when combined with input files (-i)
899+
if s.Query != "" {
896900
// If we're not in interactive mode and stdin is redirected,
897901
// we want to process all input without requiring GO statements
898902
processAll := !isInteractive
899903
err = s.Run(once, processAll)
900-
} else {
904+
}
905+
906+
// Process input files after initial query (if any)
907+
if err == nil && s.Exitcode == 0 && args.InputFile != nil {
901908
for f := range args.InputFile {
902909
if err = s.IncludeFile(args.InputFile[f], true); err != nil {
903910
s.WriteError(s.GetError(), err)
904911
s.Exitcode = 1
905912
break
906913
}
907914
}
915+
} else if args.InputFile == nil && args.Query == "" && args.InitialQuery == "" {
916+
// Interactive mode: no query, no initial query, and no input files
917+
processAll := !isInteractive
918+
err = s.Run(once, processAll)
908919
}
909920
}
910921
s.SetOutput(nil)

cmd/sqlcmd/sqlcmd_test.go

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,10 @@ func TestValidCommandLineToArgsConversion(t *testing.T) {
123123
{[]string{"-N", "true", "-J", "/path/to/cert2.pem"}, func(args SQLCmdArguments) bool {
124124
return args.EncryptConnection == "true" && args.ServerCertificate == "/path/to/cert2.pem"
125125
}},
126+
// Test -q and -i can be used together (initial query runs before input files)
127+
{[]string{"-q", "SET PARSEONLY ON", "-i", "script.sql"}, func(args SQLCmdArguments) bool {
128+
return args.InitialQuery == "SET PARSEONLY ON" && len(args.InputFile) == 1 && args.InputFile[0] == "script.sql"
129+
}},
126130
}
127131

128132
for _, test := range commands {
@@ -165,7 +169,7 @@ func TestInvalidCommandLine(t *testing.T) {
165169
commands := []cmdLineTest{
166170
{[]string{"-E", "-U", "someuser"}, "The -E and the -U/-P options are mutually exclusive."},
167171
{[]string{"-L", "-q", `"select 1"`}, "The -L parameter can not be used in combination with other parameters."},
168-
{[]string{"-i", "foo.sql", "-q", `"select 1"`}, "The i and the -Q/-q options are mutually exclusive."},
172+
{[]string{"-i", "foo.sql", "-Q", `"select 1"`}, "The -i and the -Q options are mutually exclusive."},
169173
{[]string{"-r", "5"}, "'-r 5': Unexpected argument. Argument value has to be one of [0 1]."},
170174
{[]string{"-w", "x"}, "'-w x': value must be greater than 8 and less than 65536."},
171175
{[]string{"-y", "111111"}, "'-y 111111': value must be greater than or equal to 0 and less than or equal to 8000."},
@@ -269,6 +273,35 @@ func TestRunInputFiles(t *testing.T) {
269273
}
270274
}
271275

276+
// TestInitialQueryWithInputFile verifies that -q (initial query) executes before -i (input files)
277+
func TestInitialQueryWithInputFile(t *testing.T) {
278+
o, err := os.CreateTemp("", "sqlcmdmain")
279+
assert.NoError(t, err, "os.CreateTemp")
280+
defer func() { _ = os.Remove(o.Name()) }()
281+
defer func() { _ = o.Close() }()
282+
args = newArguments()
283+
// Use -q to set NOCOUNT ON, then -i to run a select that would normally show row count
284+
// If initial query runs first, the row count message will be suppressed
285+
args.InitialQuery = "SET NOCOUNT ON"
286+
args.InputFile = []string{"testdata/select_init_value.sql"}
287+
args.OutputFile = o.Name()
288+
setAzureAuthArgIfNeeded(&args)
289+
vars := sqlcmd.InitializeVariables(args.useEnvVars())
290+
vars.Set(sqlcmd.SQLCMDMAXVARTYPEWIDTH, "0")
291+
setVars(vars, &args)
292+
293+
exitCode, err := run(vars, &args)
294+
assert.NoError(t, err, "run")
295+
assert.Equal(t, 0, exitCode, "exitCode")
296+
bytes, err := os.ReadFile(o.Name())
297+
if assert.NoError(t, err, "os.ReadFile") {
298+
// Verify that NOCOUNT ON from -q suppressed the row count message
299+
// If -q ran first, we should see "test" without "(1 row affected)"
300+
assert.Contains(t, string(bytes), "test", "Initial query should execute before input file")
301+
assert.NotContains(t, string(bytes), "row affected", "SET NOCOUNT ON should suppress row count")
302+
}
303+
}
304+
272305
func TestUnicodeOutput(t *testing.T) {
273306
o, err := os.CreateTemp("", "sqlcmdmain")
274307
assert.NoError(t, err, "os.CreateTemp")
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
select 'test' as result

0 commit comments

Comments
 (0)