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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
23 changes: 17 additions & 6 deletions cmd/sqlcmd/sqlcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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"))
Expand Down Expand Up @@ -890,21 +890,32 @@ 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
}
}
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)
s.Exitcode = 1
break
}
}
} 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)
}
}
s.SetOutput(nil)
Expand Down
33 changes: 32 additions & 1 deletion cmd/sqlcmd/sqlcmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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."},
Expand Down Expand Up @@ -269,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")
Expand Down
1 change: 1 addition & 0 deletions cmd/sqlcmd/testdata/select_init_value.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
select @@LANGUAGE
Loading