From 675912e015dc14e4c0759be24dd9cce0010026ae Mon Sep 17 00:00:00 2001 From: David Levy Date: Sat, 24 Jan 2026 21:01:51 -0600 Subject: [PATCH 1/5] Allow -q (initial query) to work with -i (input files) Previously -q and -i were mutually exclusive. Now the initial query (-q) runs first, then the input files (-i) are processed. This is useful for setting session options before running scripts. Example: sqlcmd -S server -q 'SET PARSEONLY ON' -i script.sql The -Q flag (query and exit) remains mutually exclusive with -i since -Q causes sqlcmd to exit immediately after the query. Fixes #389 --- README.md | 6 ++++++ cmd/sqlcmd/sqlcmd.go | 20 ++++++++++++++------ cmd/sqlcmd/sqlcmd_test.go | 6 +++++- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index fe26e192..89f204cc 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,12 @@ client_interface_name go-mssqldb program_name sqlcmd ``` +- 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: + +```bash +sqlcmd -S server -q "SET PARSEONLY ON" -i script.sql +``` + - `sqlcmd` supports shared memory and named pipe transport. Use the appropriate protocol prefix on the server name to force a protocol: * `lpc` for shared memory, only for a localhost. `sqlcmd -S lpc:.` * `np` for named pipes. Or use the UNC named pipe path as the server name: `sqlcmd -S \\myserver\pipe\sql\query` diff --git a/cmd/sqlcmd/sqlcmd.go b/cmd/sqlcmd/sqlcmd.go index ea655b47..8ed201ae 100644 --- a/cmd/sqlcmd/sqlcmd.go +++ b/cmd/sqlcmd/sqlcmd.go @@ -148,8 +148,8 @@ func (a *SQLCmdArguments) Validate(c *cobra.Command) (err error) { } if err == nil { switch { - case len(a.InputFile) > 0 && (len(a.Query) > 0 || len(a.InitialQuery) > 0): - err = mutuallyExclusiveError("i", `-Q/-q`) + case len(a.InputFile) > 0 && len(a.Query) > 0: + err = mutuallyExclusiveError("i", `-Q`) case a.UseTrustedConnection && (len(a.UserName) > 0 || len(a.Password) > 0): err = mutuallyExclusiveError("-E", `-U/-P`) case a.UseAad && len(a.AuthenticationMethod) > 0: @@ -400,7 +400,7 @@ func setFlags(rootCmd *cobra.Command, args *SQLCmdArguments) { rootCmd.Flags().BoolVarP(&args.Help, "help", "?", false, localizer.Sprintf("-? shows this syntax summary, %s shows modern sqlcmd sub-command help", localizer.HelpFlag)) rootCmd.Flags().StringVar(&args.TraceFile, "trace-file", "", localizer.Sprintf("Write runtime trace to the specified file. Only for advanced debugging.")) var inputfiles []string - 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)) + 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)) rootCmd.Flags().StringVarP(&args.OutputFile, "output-file", "o", "", localizer.Sprintf("Identifies the file that receives output from sqlcmd")) rootCmd.Flags().BoolVarP(&args.Version, "version", "", false, localizer.Sprintf("Print version information and exit")) rootCmd.Flags().BoolVarP(&args.TrustServerCertificate, "trust-server-certificate", "C", false, localizer.Sprintf("Implicitly trust the server certificate without validation")) @@ -891,13 +891,17 @@ func run(vars *sqlcmd.Variables, args *SQLCmdArguments) (int, error) { } else if args.InitialQuery != "" { s.Query = args.InitialQuery } - iactive := args.InputFile == nil && args.Query == "" - if iactive || s.Query != "" { + + // Run initial query (-q) if provided, even when combined with input files (-i) + if s.Query != "" { // If we're not in interactive mode and stdin is redirected, // we want to process all input without requiring GO statements processAll := !isInteractive err = s.Run(once, processAll) - } else { + } + + // Process input files after initial query (if any) + if err == nil && s.Exitcode == 0 && args.InputFile != nil { for f := range args.InputFile { if err = s.IncludeFile(args.InputFile[f], true); err != nil { s.WriteError(s.GetError(), err) @@ -905,6 +909,10 @@ func run(vars *sqlcmd.Variables, args *SQLCmdArguments) (int, error) { break } } + } else if args.InputFile == nil && args.Query == "" { + // Interactive mode: no query and no input files + processAll := !isInteractive + err = s.Run(once, processAll) } } s.SetOutput(nil) diff --git a/cmd/sqlcmd/sqlcmd_test.go b/cmd/sqlcmd/sqlcmd_test.go index 511816b2..8afea2b6 100644 --- a/cmd/sqlcmd/sqlcmd_test.go +++ b/cmd/sqlcmd/sqlcmd_test.go @@ -123,6 +123,10 @@ func TestValidCommandLineToArgsConversion(t *testing.T) { {[]string{"-N", "true", "-J", "/path/to/cert2.pem"}, func(args SQLCmdArguments) bool { return args.EncryptConnection == "true" && args.ServerCertificate == "/path/to/cert2.pem" }}, + // Test -q and -i can be used together (initial query runs before input files) + {[]string{"-q", "SET PARSEONLY ON", "-i", "script.sql"}, func(args SQLCmdArguments) bool { + return args.InitialQuery == "SET PARSEONLY ON" && len(args.InputFile) == 1 && args.InputFile[0] == "script.sql" + }}, } for _, test := range commands { @@ -165,7 +169,7 @@ func TestInvalidCommandLine(t *testing.T) { commands := []cmdLineTest{ {[]string{"-E", "-U", "someuser"}, "The -E and the -U/-P options are mutually exclusive."}, {[]string{"-L", "-q", `"select 1"`}, "The -L parameter can not be used in combination with other parameters."}, - {[]string{"-i", "foo.sql", "-q", `"select 1"`}, "The i and the -Q/-q options are mutually exclusive."}, + {[]string{"-i", "foo.sql", "-Q", `"select 1"`}, "The i and the -Q options are mutually exclusive."}, {[]string{"-r", "5"}, "'-r 5': Unexpected argument. Argument value has to be one of [0 1]."}, {[]string{"-w", "x"}, "'-w x': value must be greater than 8 and less than 65536."}, {[]string{"-y", "111111"}, "'-y 111111': value must be greater than or equal to 0 and less than or equal to 8000."}, From fdd954c9f6cb2b5ecf5b7b177691b87a814592c1 Mon Sep 17 00:00:00 2001 From: David Levy Date: Sat, 24 Jan 2026 23:03:29 -0600 Subject: [PATCH 2/5] Fix review comments: typo in error msg, prevent double Run() - Fix typo: '-i' instead of 'i' in mutuallyExclusiveError call - Add InitialQuery check to prevent double s.Run() when using -q alone --- cmd/sqlcmd/sqlcmd.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/sqlcmd/sqlcmd.go b/cmd/sqlcmd/sqlcmd.go index 8ed201ae..8c687498 100644 --- a/cmd/sqlcmd/sqlcmd.go +++ b/cmd/sqlcmd/sqlcmd.go @@ -149,7 +149,7 @@ func (a *SQLCmdArguments) Validate(c *cobra.Command) (err error) { if err == nil { switch { case len(a.InputFile) > 0 && len(a.Query) > 0: - err = mutuallyExclusiveError("i", `-Q`) + err = mutuallyExclusiveError("-i", `-Q`) case a.UseTrustedConnection && (len(a.UserName) > 0 || len(a.Password) > 0): err = mutuallyExclusiveError("-E", `-U/-P`) case a.UseAad && len(a.AuthenticationMethod) > 0: @@ -909,8 +909,8 @@ func run(vars *sqlcmd.Variables, args *SQLCmdArguments) (int, error) { break } } - } else if args.InputFile == nil && args.Query == "" { - // Interactive mode: no query and no input files + } else if args.InputFile == nil && args.Query == "" && args.InitialQuery == "" { + // Interactive mode: no query, no initial query, and no input files processAll := !isInteractive err = s.Run(once, processAll) } From d625c0e2301d7a609cd1eefd8723c39eb3a27019 Mon Sep 17 00:00:00 2001 From: David Levy Date: Sun, 25 Jan 2026 12:03:50 -0600 Subject: [PATCH 3/5] Fix test to expect corrected -i flag error message --- cmd/sqlcmd/sqlcmd_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/sqlcmd/sqlcmd_test.go b/cmd/sqlcmd/sqlcmd_test.go index 8afea2b6..731557b1 100644 --- a/cmd/sqlcmd/sqlcmd_test.go +++ b/cmd/sqlcmd/sqlcmd_test.go @@ -169,7 +169,7 @@ func TestInvalidCommandLine(t *testing.T) { commands := []cmdLineTest{ {[]string{"-E", "-U", "someuser"}, "The -E and the -U/-P options are mutually exclusive."}, {[]string{"-L", "-q", `"select 1"`}, "The -L parameter can not be used in combination with other parameters."}, - {[]string{"-i", "foo.sql", "-Q", `"select 1"`}, "The i and the -Q options are mutually exclusive."}, + {[]string{"-i", "foo.sql", "-Q", `"select 1"`}, "The -i and the -Q options are mutually exclusive."}, {[]string{"-r", "5"}, "'-r 5': Unexpected argument. Argument value has to be one of [0 1]."}, {[]string{"-w", "x"}, "'-w x': value must be greater than 8 and less than 65536."}, {[]string{"-y", "111111"}, "'-y 111111': value must be greater than or equal to 0 and less than or equal to 8000."}, From 5d3d2edda18176b77bdee5e9b8356138ffaec375 Mon Sep 17 00:00:00 2001 From: David Levy Date: Sun, 25 Jan 2026 12:21:57 -0600 Subject: [PATCH 4/5] Add integration test for -q with -i execution order --- cmd/sqlcmd/sqlcmd_test.go | 27 +++++++++++++++++++++++ cmd/sqlcmd/testdata/select_init_value.sql | 1 + 2 files changed, 28 insertions(+) create mode 100644 cmd/sqlcmd/testdata/select_init_value.sql diff --git a/cmd/sqlcmd/sqlcmd_test.go b/cmd/sqlcmd/sqlcmd_test.go index 731557b1..5cf28c84 100644 --- a/cmd/sqlcmd/sqlcmd_test.go +++ b/cmd/sqlcmd/sqlcmd_test.go @@ -273,6 +273,33 @@ func TestRunInputFiles(t *testing.T) { } } +// TestInitialQueryWithInputFile verifies that -q (initial query) executes before -i (input files) +func TestInitialQueryWithInputFile(t *testing.T) { + o, err := os.CreateTemp("", "sqlcmdmain") + assert.NoError(t, err, "os.CreateTemp") + defer os.Remove(o.Name()) + defer o.Close() + args = newArguments() + // Use -q to change session language, then -i to verify the setting persists + // The initial query sets LANGUAGE to German, then the script selects @@LANGUAGE + args.InitialQuery = "SET LANGUAGE German" + args.InputFile = []string{"testdata/select_init_value.sql"} + args.OutputFile = o.Name() + setAzureAuthArgIfNeeded(&args) + vars := sqlcmd.InitializeVariables(args.useEnvVars()) + vars.Set(sqlcmd.SQLCMDMAXVARTYPEWIDTH, "0") + setVars(vars, &args) + + exitCode, err := run(vars, &args) + assert.NoError(t, err, "run") + assert.Equal(t, 0, exitCode, "exitCode") + bytes, err := os.ReadFile(o.Name()) + if assert.NoError(t, err, "os.ReadFile") { + // Verify that the language set in the initial query is reflected in the script output + assert.Contains(t, string(bytes), "Deutsch", "Initial query should execute before input file") + } +} + func TestUnicodeOutput(t *testing.T) { o, err := os.CreateTemp("", "sqlcmdmain") assert.NoError(t, err, "os.CreateTemp") diff --git a/cmd/sqlcmd/testdata/select_init_value.sql b/cmd/sqlcmd/testdata/select_init_value.sql new file mode 100644 index 00000000..169128e5 --- /dev/null +++ b/cmd/sqlcmd/testdata/select_init_value.sql @@ -0,0 +1 @@ +select @@LANGUAGE From f0f2cad3d36d6d17f36267f20005d72d1715f9c7 Mon Sep 17 00:00:00 2001 From: David Levy Date: Sun, 25 Jan 2026 12:54:34 -0600 Subject: [PATCH 5/5] Address Copilot review: fix quote style, prevent nil pointer in non-interactive mode --- cmd/sqlcmd/sqlcmd.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cmd/sqlcmd/sqlcmd.go b/cmd/sqlcmd/sqlcmd.go index 8c687498..e55697ec 100644 --- a/cmd/sqlcmd/sqlcmd.go +++ b/cmd/sqlcmd/sqlcmd.go @@ -149,7 +149,7 @@ func (a *SQLCmdArguments) Validate(c *cobra.Command) (err error) { if err == nil { switch { case len(a.InputFile) > 0 && len(a.Query) > 0: - err = mutuallyExclusiveError("-i", `-Q`) + err = mutuallyExclusiveError("-i", "-Q") case a.UseTrustedConnection && (len(a.UserName) > 0 || len(a.Password) > 0): err = mutuallyExclusiveError("-E", `-U/-P`) case a.UseAad && len(a.AuthenticationMethod) > 0: @@ -890,6 +890,9 @@ func run(vars *sqlcmd.Variables, args *SQLCmdArguments) (int, error) { s.Query = args.Query } else if args.InitialQuery != "" { s.Query = args.InitialQuery + if !isInteractive { + once = true + } } // Run initial query (-q) if provided, even when combined with input files (-i)