Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
170f439
feat: add -f codepage flag for input/output encoding
dlevy-msft-sql Jan 25, 2026
5309eda
Merge branch 'main' into round-out-part2
dlevy-msft-sql Jan 25, 2026
8a74e3b
Address Copilot review comments on codepage support
dlevy-msft-sql Jan 25, 2026
faa945c
Fix ineffectual assignment lint error
dlevy-msft-sql Jan 25, 2026
424cc32
Address Copilot review comments for codepage support
dlevy-msft-sql Jan 25, 2026
2a14b75
Fix locale-specific number formatting in codepage error
dlevy-msft-sql Jan 25, 2026
06da044
refactor: consolidate codepage definitions into single registry
dlevy-msft-sql Jan 25, 2026
065b1f4
test: add TestSupportedCodePages to verify registry consistency
dlevy-msft-sql Jan 25, 2026
9b0cd91
Fix UTF-16 BOM handling and add Windows codepage fallback
dlevy-msft-sql Jan 25, 2026
ff05619
Address Copilot review comments for codepage implementation
dlevy-msft-sql Jan 25, 2026
2e62e10
Address Copilot review comments (round 7)
dlevy-msft-sql Jan 25, 2026
b96e228
Fix codepage error message for locale-independent formatting
dlevy-msft-sql Jan 26, 2026
4ddfd1a
Fix Windows codepage transformer to handle streaming correctly
dlevy-msft-sql Jan 26, 2026
9c471b1
Address Copilot review: clarify BOM handling documentation
dlevy-msft-sql Jan 26, 2026
4f14d0f
Merge branch 'main' into round-out-part2
dlevy-msft-sql Jan 27, 2026
a672f7f
Merge branch 'main' into round-out-part2
dlevy-msft-sql Jan 28, 2026
c05fae4
docs: add differences from ODBC sqlcmd section for codepage support
dlevy-msft-sql Jan 28, 2026
b284793
Merge branch 'main' into round-out-part2
dlevy-msft-sql Jan 28, 2026
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
74 changes: 74 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ The following switches have different behavior in this version of `sqlcmd` compa
- To provide the value of the host name in the server certificate when using strict encryption, pass the host name with `-F`. Example: `-Ns -F myhost.domain.com`
- More information about client/server encryption negotiation can be found at <https://docs.microsoft.com/openspecs/windows_protocols/ms-tds/60f56408-0188-4cd5-8b90-25c6f2423868>
- `-u` The generated Unicode output file will have the UTF16 Little-Endian Byte-order mark (BOM) written to it.
- `-f` Specifies the code page for input and output files. See [Code Page Support](#code-page-support) below for details and examples.
- Some behaviors that were kept to maintain compatibility with `OSQL` may be changed, such as alignment of column headers for some data types.
- 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.
- `-i` doesn't handle a comma `,` in a file name correctly unless the file name argument is triple quoted. For example:
Expand Down Expand Up @@ -255,6 +256,79 @@ To see a list of available styles along with colored syntax samples, use this co
:list color
```

### Code Page Support

The `-f` flag specifies the code page for reading input files and writing output. This is useful when working with SQL scripts saved in legacy encodings or when output needs to be in a specific encoding.

#### Format

```
-f codepage # Set both input and output to the same codepage
-f i:codepage # Set input codepage only
-f o:codepage # Set output codepage only
-f i:codepage,o:codepage # Set input and output to different codepages
-f o:codepage,i:codepage # Same as above (order doesn't matter)
```

#### Common Code Pages

| Code Page | Name | Description |
|-----------|------|-------------|
| 65001 | UTF-8 | Unicode (UTF-8) - default for most modern systems |
| 1200 | UTF-16LE | Unicode (UTF-16 Little-Endian) |
| 1201 | UTF-16BE | Unicode (UTF-16 Big-Endian) |
| 1252 | Windows-1252 | Western European (Windows) |
| 932 | Shift_JIS | Japanese |
| 936 | GBK | Chinese Simplified |
| 949 | EUC-KR | Korean |
| 950 | Big5 | Chinese Traditional |
| 437 | CP437 | OEM United States (DOS) |

#### Examples

**Run a script saved in Windows-1252 encoding:**
```bash
sqlcmd -S myserver -i legacy_script.sql -f 1252
```

**Read UTF-16 input file and write UTF-8 output:**
```bash
sqlcmd -S myserver -i unicode_script.sql -o results.txt -f i:1200,o:65001
```

**Process a Japanese Shift-JIS encoded script:**
```bash
sqlcmd -S myserver -i japanese_data.sql -f 932
```

**Write output in Windows-1252 for legacy applications:**
```bash
sqlcmd -S myserver -Q "SELECT * FROM Products" -o report.txt -f o:1252
```

**List all supported code pages:**
```bash
sqlcmd --list-codepages
```

#### Notes

- When no `-f` flag is specified, sqlcmd auto-detects UTF-8/UTF-16LE/UTF-16BE BOM (Byte Order Mark) in input files and switches to the appropriate decoder. If no BOM is present, UTF-8 is assumed.
- UTF-8 input files with BOM are handled automatically.
- On Windows, additional codepages installed on the system are available via the Windows API, even if not shown by `--list-codepages`.
- Use `--list-codepages` to see the built-in code pages with their names and descriptions.

#### Differences from ODBC sqlcmd

| Aspect | ODBC sqlcmd | go-sqlcmd |
|--------|-------------|-----------|
| **Default encoding (no BOM, no `-f`)** | Windows ANSI code page (locale-dependent, e.g., 1252) | UTF-8 |
| **UTF-16 codepages (1200, 1201)** | Rejected by `IsValidCodePage()` API | Accepted |
| **BOM detection** | Yes (UTF-8, UTF-16 LE/BE) | Yes (identical behavior) |
| **`--list-codepages`** | Not available | Available |

**Migration note**: If you have UTF-8 encoded SQL scripts without a BOM that worked with ODBC sqlcmd on Windows, they should work identically or better with go-sqlcmd since go-sqlcmd defaults to UTF-8. However, if you have scripts in Windows ANSI encoding (e.g., Windows-1252) without a BOM, you may need to explicitly specify `-f 1252` with go-sqlcmd.

### Packages

#### sqlcmd executable
Expand Down
29 changes: 29 additions & 0 deletions cmd/sqlcmd/sqlcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ type SQLCmdArguments struct {
ChangePassword string
ChangePasswordAndExit string
TraceFile string
CodePage string
// codePageSettings stores the parsed CodePageSettings after validation.
// This avoids parsing CodePage twice (in Validate and run).
codePageSettings *sqlcmd.CodePageSettings
ListCodePages bool
// Keep Help at the end of the list
Help bool
}
Expand Down Expand Up @@ -171,6 +176,12 @@ func (a *SQLCmdArguments) Validate(c *cobra.Command) (err error) {
err = rangeParameterError("-t", fmt.Sprint(a.QueryTimeout), 0, 65534, true)
case a.ServerCertificate != "" && !encryptConnectionAllowsTLS(a.EncryptConnection):
err = localizer.Errorf("The -J parameter requires encryption to be enabled (-N true, -N mandatory, or -N strict).")
case a.CodePage != "":
if codePageSettings, parseErr := sqlcmd.ParseCodePage(a.CodePage); parseErr != nil {
err = localizer.Errorf(`'-f %s': %v`, a.CodePage, parseErr)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lizer.Errorf

there's no localizable content in the format string so don't need to use localizer

} else {
a.codePageSettings = codePageSettings
}
}
}
if err != nil {
Expand Down Expand Up @@ -239,6 +250,17 @@ func Execute(version string) {
listLocalServers()
os.Exit(0)
}
// List supported codepages
if args.ListCodePages {
fmt.Println(localizer.Sprintf("Supported Code Pages:"))
fmt.Println()
fmt.Printf("%-8s %-20s %s\n", "Code", "Name", "Description")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

", "Code", "Name", "Description")

does this string need to be localizable?

fmt.Printf("%-8s %-20s %s\n", "----", "----", "-----------")
for _, cp := range sqlcmd.SupportedCodePages() {
fmt.Printf("%-8d %-20s %s\n", cp.CodePage, cp.Name, cp.Description)
}
os.Exit(0)
}
if len(argss) > 0 {
fmt.Printf("%s'%s': Unknown command. Enter '--help' for command help.", sqlcmdErrorPrefix, argss[0])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

%s'%s': Unknown command. Enter '--help' for command help

does this string need to be localizable?

os.Exit(1)
Expand Down Expand Up @@ -479,6 +501,8 @@ func setFlags(rootCmd *cobra.Command, args *SQLCmdArguments) {
rootCmd.Flags().BoolVarP(&args.EnableColumnEncryption, "enable-column-encryption", "g", false, localizer.Sprintf("Enable column encryption"))
rootCmd.Flags().StringVarP(&args.ChangePassword, "change-password", "z", "", localizer.Sprintf("New password"))
rootCmd.Flags().StringVarP(&args.ChangePasswordAndExit, "change-password-exit", "Z", "", localizer.Sprintf("New password and exit"))
rootCmd.Flags().StringVarP(&args.CodePage, "code-page", "f", "", localizer.Sprintf("Specifies the code page for input/output. Use 65001 for UTF-8. Format: codepage | i:codepage[,o:codepage] | o:codepage[,i:codepage]"))
rootCmd.Flags().BoolVar(&args.ListCodePages, "list-codepages", false, localizer.Sprintf("List supported code pages and exit"))
}

func setScriptVariable(v string) string {
Expand Down Expand Up @@ -817,6 +841,11 @@ func run(vars *sqlcmd.Variables, args *SQLCmdArguments) (int, error) {
defer s.StopCloseHandler()
s.UnicodeOutputFile = args.UnicodeOutputFile

// Apply codepage settings (already parsed and validated in Validate)
if args.codePageSettings != nil {
s.CodePage = args.codePageSettings
}

if args.DisableCmd != nil {
s.Cmd.DisableSysCommands(args.errorOnBlockedCmd())
}
Expand Down
21 changes: 21 additions & 0 deletions cmd/sqlcmd/sqlcmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,22 @@ 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"
}},
// Codepage flag tests
{[]string{"-f", "65001"}, func(args SQLCmdArguments) bool {
return args.CodePage == "65001"
}},
{[]string{"-f", "i:1252,o:65001"}, func(args SQLCmdArguments) bool {
return args.CodePage == "i:1252,o:65001"
}},
{[]string{"-f", "o:65001,i:1252"}, func(args SQLCmdArguments) bool {
return args.CodePage == "o:65001,i:1252"
}},
{[]string{"--code-page", "1252"}, func(args SQLCmdArguments) bool {
return args.CodePage == "1252"
}},
{[]string{"--list-codepages"}, func(args SQLCmdArguments) bool {
return args.ListCodePages
}},
}

for _, test := range commands {
Expand Down Expand Up @@ -178,6 +194,11 @@ func TestInvalidCommandLine(t *testing.T) {
{[]string{"-N", "optional", "-J", "/path/to/cert.pem"}, "The -J parameter requires encryption to be enabled (-N true, -N mandatory, or -N strict)."},
{[]string{"-N", "disable", "-J", "/path/to/cert.pem"}, "The -J parameter requires encryption to be enabled (-N true, -N mandatory, or -N strict)."},
{[]string{"-N", "strict", "-F", "myserver.domain.com", "-J", "/path/to/cert.pem"}, "The -F and the -J options are mutually exclusive."},
// Codepage validation tests
{[]string{"-f", "invalid"}, `'-f invalid': invalid codepage: invalid`},
{[]string{"-f", "99999"}, `'-f 99999': unsupported codepage 99999`},
{[]string{"-f", "i:invalid"}, `'-f i:invalid': invalid input codepage: i:invalid`},
{[]string{"-f", "x:1252"}, `'-f x:1252': invalid codepage: x:1252`},
}

for _, test := range commands {
Expand Down
Loading
Loading