-
Notifications
You must be signed in to change notification settings - Fork 79
Implement -R flag for regional settings #628
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
- 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.
There was a problem hiding this 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
RegionalSettingswith platform-specific locale detection (detectUserLocale) and integrate it into the default formatter so that-Rcontrols 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-pageand--list-codepages. - Extend tests to cover regional formatting helpers, formatter construction, code page parsing/encoding, CLI argument parsing/validation, and document new
-Rand-fbehaviors inREADME.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.
pkg/sqlcmd/commands.go
Outdated
| encoder := transform.NewWriter(o, enc.NewEncoder()) | ||
| s.SetError(encoder) |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
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.
| 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) | |
| } |
pkg/sqlcmd/format.go
Outdated
| 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 { |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
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.
| // 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 |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
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.
| // 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 |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
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.
| // 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 |
| 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")) |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
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.
| - `-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. |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
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.
pkg/sqlcmd/commands.go
Outdated
| if s.CodePage != nil && s.CodePage.OutputCodePage != 0 { | ||
| enc, err := GetEncoding(s.CodePage.OutputCodePage) | ||
| if err != nil { | ||
| o.Close() |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
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.
| o.Close() | |
| if cerr := o.Close(); cerr != nil { | |
| return fmt.Errorf("%w (and closing error file %s failed: %v)", err, filePath, cerr) | |
| } |
- 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
Summary
Implements the -R\ flag to enable locale-aware formatting for output, matching ODBC sqlcmd behavior.
Changes
Motivation
Customers migrating from ODBC sqlcmd expect the -R\ flag to produce locale-aware output. Previously this was a no-op stub.
Testing