Skip to content

Conversation

@dlevy-msft-sql
Copy link
Contributor

Summary

Implements the -R\ flag to enable locale-aware formatting for output, matching ODBC sqlcmd behavior.

Changes

  • Add \RegionalSettings\ struct with locale-aware formatting methods
  • Detect user locale from Windows LCID or Unix environment variables (\LC_ALL, \LC_MESSAGES, \LANG)
  • Apply regional formatting to:
    • DECIMAL/NUMERIC: Numbers with locale-specific thousand separators
    • MONEY/SMALLMONEY: Currency with locale-specific thousand separators
    • DATE: Locale-specific date format (e.g., MM/DD/YYYY for en-US, DD/MM/YYYY for en-GB)
    • TIME/DATETIME/DATETIME2/DATETIMEOFFSET: Locale-specific time format (12/24 hour)
  • Add comprehensive tests for regional formatting
  • Update README to document the -R\ flag behavior

Motivation

Customers migrating from ODBC sqlcmd expect the -R\ flag to produce locale-aware output. Previously this was a no-op stub.

Testing

  • Added unit tests for all formatting functions
  • All existing tests pass

- Add -f/--code-page flag with ODBC-compatible format parsing

- Support 50+ codepages: Unicode, Windows, OEM/DOS, ISO-8859, CJK, EBCDIC, Macintosh

- Apply input codepage in IncludeFile() for :r command

- Apply output codepage in outCommand() for :OUT file writes

- Add --list-codepages flag to display all supported codepages

- Add comprehensive unit tests for parsing and encoding lookup
- Add RegionalSettings struct with locale-aware formatting for numbers,
  currency, dates, and times
- Detect user locale from Windows LCID or Unix environment variables
- Apply regional formatting to DECIMAL, NUMERIC, MONEY, SMALLMONEY,
  DATE, TIME, DATETIME, DATETIME2, DATETIMEOFFSET types
- Add comprehensive tests for regional formatting
- Update README to document the -R flag behavior

This enables customers migrating from ODBC sqlcmd to see the same
locale-aware output formatting when using the -R flag.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements locale-aware formatting for query results when -R is specified and introduces configurable code page handling for input/output, plus associated CLI flags and documentation updates.

Changes:

  • Add RegionalSettings with platform-specific locale detection (detectUserLocale) and integrate it into the default formatter so that -R controls locale-aware numeric and date/time formatting.
  • Introduce code page parsing and encoding support (ParseCodePage, GetEncoding, SupportedCodePages) and wire it into file input (:R), output (:OUT, :ERROR), and new CLI flags -f/--code-page and --list-codepages.
  • Extend tests to cover regional formatting helpers, formatter construction, code page parsing/encoding, CLI argument parsing/validation, and document new -R and -f behaviors in README.md.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
pkg/sqlcmd/sqlcmd.go Adds CodePage *CodePageSettings to Sqlcmd and updates IncludeFile to honor configured input code pages or BOM-based UTF-16 auto-detection when reading :R files.
pkg/sqlcmd/regional.go Implements RegionalSettings (locale detection via detectUserLocale), number/money/date/time formatting, and locale-specific separators and date/time layouts.
pkg/sqlcmd/regional_windows.go Windows-only locale detection using GetUserDefaultLCID and a mapping from LCID values to BCP 47 language tags.
pkg/sqlcmd/regional_linux.go Linux-only locale detection from LC_ALL, LC_MESSAGES, and LANG, plus Unix locale string parsing to BCP 47 tags.
pkg/sqlcmd/regional_darwin.go macOS-only locale detection using environment variables or defaults read -g AppleLocale, with Unix locale parsing similar to Linux.
pkg/sqlcmd/regional_test.go Unit tests for RegionalSettings enable/disable behavior, NULL/empty handling, separators, date/time format selection, helper functions, and basic formatter construction with/without regional settings.
pkg/sqlcmd/format.go Extends sqlCmdFormatterType with a regional *RegionalSettings field, adds NewSQLCmdDefaultFormatterWithRegional, and applies regional formatting to numeric and date/time columns in scanRow when -R is enabled.
pkg/sqlcmd/commands.go Updates :OUT and :ERROR commands to write using either UTF-16 (for -u) or a configured output code page via GetEncoding, falling back to raw UTF-8 when appropriate.
pkg/sqlcmd/codepage.go Adds CodePageSettings, ParseCodePage for -f syntax, GetEncoding for many Windows and related code pages, and SupportedCodePages metadata for listing.
pkg/sqlcmd/codepage_test.go Tests ParseCodePage (including error cases and specific code pages) and GetEncoding for successful encodings and error handling for unsupported code pages.
cmd/sqlcmd/sqlcmd.go Extends SQLCmdArguments with CodePage, ListCodePages, and UseRegionalSettings, validates -f, adds --code-page, --list-codepages, and -R flag wiring, lists supported code pages when requested, parses code page settings before running, and uses NewSQLCmdDefaultFormatterWithRegional to honor -R.
cmd/sqlcmd/sqlcmd_test.go Adds CLI argument parsing tests for -f variations and --list-codepages, plus invalid -f cases that exercise Validate; reuses existing test harness for command-line normalization and error formatting.
README.md Updates the description of -R to reflect new locale-aware formatting behavior and documents the new -f code page option and --list-codepages helper, including examples of supported code pages.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 376 to 377
encoder := transform.NewWriter(o, enc.NewEncoder())
s.SetError(encoder)
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

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

When a UTF-8 code page is configured (e.g., -f 65001 which sets OutputCodePage to 65001), GetEncoding returns nil to signal no transformation is needed, but this branch unconditionally calls enc.NewEncoder(). That will panic when enc is nil, breaking :ERROR redirection under UTF-8. You should mirror the OUT handling above by checking for a nil encoder and either skipping the transform or setting the raw file writer directly when the selected code page is UTF-8.

Suggested change
encoder := transform.NewWriter(o, enc.NewEncoder())
s.SetError(encoder)
if enc == nil {
// No transformation required (e.g., UTF-8), write directly
s.SetError(o)
} else {
encoder := transform.NewWriter(o, enc.NewEncoder())
s.SetError(encoder)
}

Copilot uses AI. Check for mistakes.
Comment on lines 571 to 585
val := fmt.Sprintf("%v", x)
// Apply regional formatting for numeric types
if f.regional.IsEnabled() {
switch typeName {
case "DECIMAL", "NUMERIC":
row[n] = f.regional.FormatNumber(val)
case "MONEY", "SMALLMONEY":
row[n] = f.regional.FormatMoney(val)
default:
row[n] = val
}
} else {
row[n] = val
}
if err != nil {
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

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

This default branch declares an err variable and checks if err != nil but never assigns to it after the refactor; val := fmt.Sprintf("%v", x) does not return an error. The error check is now dead code and can be removed along with the unused err variable to simplify the control flow and avoid confusion about possible failure paths.

Copilot uses AI. Check for mistakes.
Comment on lines +59 to +76
// Try to parse as float to use the message printer
if f, err := strconv.ParseFloat(strings.Replace(value, ",", "", -1), 64); err == nil {
// Use the message printer with the number formatter for grouping
formatted := r.printer.Sprint(number.Decimal(f))
if negative && !strings.HasPrefix(formatted, "-") {
formatted = "-" + formatted
}
return formatted
}

// Fallback for very large numbers that don't fit in float64
// Add thousand separators manually using locale convention
formatted := addThousandSeparators(intPart, r.tag)
if len(parts) > 1 {
formatted += getDecimalSeparator(r.tag) + parts[1]
}
if negative {
formatted = "-" + formatted
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

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

FormatNumber parses the DECIMAL/NUMERIC string into a float64 (strconv.ParseFloat) and then formats it with number.Decimal, which will silently lose precision for values beyond float64’s ~15–17 significant digits (SQL Server DECIMAL/NUMERIC can have up to 38 digits). This means -R can produce rounded or otherwise inaccurate output compared to the original string. To preserve exact values, avoid float64 here and instead insert thousand and decimal separators purely by string manipulation (similar to the manual addThousandSeparators fallback) for all inputs.

Copilot uses AI. Check for mistakes.
Comment on lines +88 to +106
// Parse the money value
negative := strings.HasPrefix(value, "-")
cleanValue := value
if negative {
cleanValue = value[1:]
}

if f, err := strconv.ParseFloat(cleanValue, 64); err == nil {
// Use locale-aware number formatting
// Note: We use number formatting, not currency, to maintain compatibility
// with ODBC sqlcmd which formats with thousand separators but not currency symbols
formatted := r.printer.Sprint(number.Decimal(f, number.Scale(4)))
if negative && !strings.HasPrefix(formatted, "-") {
formatted = "-" + formatted
}
return formatted
}

return value
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

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

FormatMoney also parses the MONEY/SMALLMONEY string into a float64 and formats via number.Decimal, which can introduce rounding for larger values because MONEY values can exceed float64’s exact integer range while still requiring 4 fractional digits. For strict compatibility with SQL Server and ODBC sqlcmd, this should avoid float64 and instead format the integer and fractional parts as strings (preserving all digits) while inserting locale-specific thousand and decimal separators.

Suggested change
// Parse the money value
negative := strings.HasPrefix(value, "-")
cleanValue := value
if negative {
cleanValue = value[1:]
}
if f, err := strconv.ParseFloat(cleanValue, 64); err == nil {
// Use locale-aware number formatting
// Note: We use number formatting, not currency, to maintain compatibility
// with ODBC sqlcmd which formats with thousand separators but not currency symbols
formatted := r.printer.Sprint(number.Decimal(f, number.Scale(4)))
if negative && !strings.HasPrefix(formatted, "-") {
formatted = "-" + formatted
}
return formatted
}
return value
// MONEY/SMALLMONEY are fixed-point with 4 decimal places.
// Avoid float64 to prevent rounding of large values; format via string operations.
negative := strings.HasPrefix(value, "-")
cleanValue := value
if negative {
cleanValue = value[1:]
}
// Split into integer and fractional parts
parts := strings.SplitN(cleanValue, ".", 2)
intPart := parts[0]
fracPart := ""
if len(parts) > 1 {
fracPart = parts[1]
}
// Normalize fractional part to exactly 4 digits, matching SQL Server MONEY display
if len(fracPart) == 0 {
fracPart = "0000"
} else if len(fracPart) < 4 {
fracPart = fracPart + strings.Repeat("0", 4-len(fracPart))
} else if len(fracPart) > 4 {
fracPart = fracPart[:4]
}
// Apply locale-specific thousand separators to the integer part
formattedInt := addThousandSeparators(intPart, r.tag)
// Combine with locale-specific decimal separator
formatted := formattedInt + getDecimalSeparator(r.tag) + fracPart
if negative {
formatted = "-" + formatted
}
return formatted

Copilot uses AI. Check for mistakes.
rootCmd.Flags().BoolVarP(&args.DedicatedAdminConnection, "dedicated-admin-connection", "A", false, localizer.Sprintf("Dedicated administrator connection"))
_ = rootCmd.Flags().BoolP("enable-quoted-identifiers", "I", true, localizer.Sprintf("Provided for backward compatibility. Quoted identifiers are always enabled"))
_ = rootCmd.Flags().BoolP("client-regional-setting", "R", false, localizer.Sprintf("Provided for backward compatibility. Client regional settings are not used"))
rootCmd.Flags().BoolVarP(&args.UseRegionalSettings, "client-regional-setting", "R", false, localizer.Sprintf("Use client regional settings for currency, date, and time formatting"))
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

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

UseRegionalSettings and the -R/--client-regional-setting flag are newly wired here, but there is no corresponding CLI-to-argument test in TestValidCommandLineToArgsConversion (that test already covers many other flags, including the new -f/--code-page and --list-codepages). To keep parity and prevent regressions in -R handling, please add a test case asserting that -R (and optionally --client-regional-setting) correctly set args.UseRegionalSettings to true.

Copilot uses AI. Check for mistakes.
Comment on lines 135 to +136
- `-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. Format: `codepage | i:codepage[,o:codepage] | o:codepage[,i:codepage]`. Use `65001` for UTF-8. Supported codepages include Unicode (65001, 1200, 1201), Windows (874, 1250-1258), OEM/DOS (437, 850, etc.), ISO-8859 (28591-28606), CJK (932, 936, 949, 950), and EBCDIC (37, 1047, 1140). Use `--list-codepages` to see all supported code pages.
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

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

This change adds -f/--code-page support and --list-codepages behavior, which is not mentioned in the PR description that focuses on implementing -R regional settings. Since code page handling is a separate, user-visible feature with its own semantics and error paths, please update the PR description (and any linked issue) to document this behavior explicitly so reviewers and users understand that code page support is being introduced here as well.

Copilot uses AI. Check for mistakes.
if s.CodePage != nil && s.CodePage.OutputCodePage != 0 {
enc, err := GetEncoding(s.CodePage.OutputCodePage)
if err != nil {
o.Close()
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

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

File handle may be writable as a result of data flow from a call to OpenFile and closing it may result in data loss upon failure, which is not handled explicitly.

Suggested change
o.Close()
if cerr := o.Close(); cerr != nil {
return fmt.Errorf("%w (and closing error file %s failed: %v)", err, filePath, cerr)
}

Copilot uses AI. Check for mistakes.
- Fix nil encoder panic in errorCommand when using UTF-8 codepage
- Improve error handling with proper file close on encoding error
- Remove dead code (unused 'err' variable) in format.go
- Add missing -R flag test in TestValidCommandLineToArgsConversion
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant