Skip to content
Closed
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
49 changes: 45 additions & 4 deletions public/Import-DbaCsv.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,47 @@ function Import-DbaCsv {
Write-Message -Level Warning -Message "Both SampleRows and DetectColumnTypes specified. DetectColumnTypes (full scan) takes precedence for zero-risk type detection."
}

# When Culture is specified but DateTimeFormats is not, derive unambiguous datetime format
# strings that force ParseExact (which respects day/month field order) instead of
# DateTime.Parse (which can swap day/month when both values are <= 12).
# See: https://github.com/dataplat/dbatools/issues/10338
#
# Design notes:
# 1. We do NOT use $dtf.ShortDatePattern or $dtf.LongTimePattern directly because these
# properties differ between Windows (NLS) and Linux (ICU), causing platform-specific
# format mismatches in CI. Instead we derive the format from stable culture primitives.
# 2. Time colons are escaped as HH':'mm':'ss rather than HH:mm:ss. In DateTime.ParseExact,
# an unescaped ':' is a placeholder for the culture's TimeSeparator property. For de-CH
# on Linux/ICU the TimeSeparator is '.' not ':', so HH:mm:ss would expect "17.09.41"
# and fail to match CSV data that uses the common "17:09:41" format. Escaping forces
# a literal ':' match regardless of the culture's TimeSeparator.
if ($PSBoundParameters.Culture -and -not $PSBoundParameters.DateTimeFormats) {
$cultureObj = New-Object System.Globalization.CultureInfo($Culture)
$dtf = $cultureObj.DateTimeFormat
$dateSep = $dtf.DateSeparator

# Determine date field order from the first character of ShortDatePattern
if ($dtf.ShortDatePattern -match '^y') {
# Year-first cultures (e.g. yyyy-MM-dd)
$datePattern = "yyyy${dateSep}MM${dateSep}dd"
} elseif ($dtf.ShortDatePattern -match '^M') {
# Month-first cultures (e.g. MM/dd/yyyy for en-US)
$datePattern = "MM${dateSep}dd${dateSep}yyyy"
} else {
# Day-first cultures (e.g. dd.MM.yyyy for de-CH, de-DE, en-GB)
$datePattern = "dd${dateSep}MM${dateSep}yyyy"
}

$effectiveDateTimeFormats = @(
"$datePattern HH':'mm':'ss",
"$datePattern HH':'mm",
$datePattern
)
Write-Message -Level Verbose -Message "Derived DateTimeFormats from Culture '$Culture': $($effectiveDateTimeFormats -join ', ')"
} elseif ($PSBoundParameters.DateTimeFormats) {
$effectiveDateTimeFormats = $DateTimeFormats
}

function New-SqlTable {
<#
.SYNOPSIS
Expand Down Expand Up @@ -1142,8 +1183,8 @@ WHERE c.object_id = OBJECT_ID(@tableName)
} else {
$inferOptions.AllowMultilineFields = $SupportsMultiline.IsPresent
}
if ($PSBoundParameters.DateTimeFormats) {
$inferOptions.DateTimeFormats = $DateTimeFormats
if ($effectiveDateTimeFormats) {
$inferOptions.DateTimeFormats = $effectiveDateTimeFormats
}
if ($PSBoundParameters.Culture) {
$inferOptions.Culture = New-Object System.Globalization.CultureInfo($Culture)
Expand Down Expand Up @@ -1348,8 +1389,8 @@ WHERE c.object_id = OBJECT_ID(@tableName)
$csvOptions.MaxQuotedFieldLength = $MaxQuotedFieldLength
}
$csvOptions.ParseErrorAction = [Dataplat.Dbatools.Csv.CsvParseErrorAction]::$ParseErrorAction
if ($PSBoundParameters.DateTimeFormats) {
$csvOptions.DateTimeFormats = $DateTimeFormats
if ($effectiveDateTimeFormats) {
$csvOptions.DateTimeFormats = $effectiveDateTimeFormats
}
if ($PSBoundParameters.Culture) {
$csvOptions.Culture = New-Object System.Globalization.CultureInfo($Culture)
Expand Down
38 changes: 38 additions & 0 deletions tests/Import-DbaCsv.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,44 @@ Describe $CommandName -Tag IntegrationTests {
Invoke-DbaQuery -SqlInstance $server -Query "DROP TABLE $tableName" -ErrorAction SilentlyContinue
Remove-Item $filePath -ErrorAction SilentlyContinue
}

It "correctly parses ambiguous dates (day and month both <= 12) using Culture datetime format (issue #10338)" {
$filePath = "$($TestConfig.Temp)\culture-datetime-$(Get-Random).csv"
$server = Connect-DbaInstance $TestConfig.InstanceMulti1 -Database tempdb
$tableName = "CultureDateTimeTest$(Get-Random)"

# Create CSV with de-CH style dates (dd.MM.yyyy HH:mm:ss, semicolon delimiter)
# Row 1: day=2, month=4 (both <= 12, ambiguous - previously parsed as Feb 4th)
# Row 2: day=11, month=6 (both <= 12, ambiguous - previously parsed as Nov 6th)
"Date1;Date2" | Out-File -FilePath $filePath -Encoding UTF8
"02.04.2026 17:09:41;14.04.2026 17:09:41" | Out-File -FilePath $filePath -Encoding UTF8 -Append
"11.06.2026 17:10:08;13.06.2026 06:22:23" | Out-File -FilePath $filePath -Encoding UTF8 -Append

# Create table with smalldatetime columns
Invoke-DbaQuery -SqlInstance $server -Query "CREATE TABLE $tableName (Date1 smalldatetime NULL, Date2 smalldatetime NULL)"

$result = Import-DbaCsv -Path $filePath -SqlInstance $server -Database tempdb -Table $tableName -Delimiter ";" -Culture "de-CH"

$result.RowsCopied | Should -Be 2

$data = Invoke-DbaQuery -SqlInstance $server -Query "SELECT * FROM $tableName ORDER BY Date1" -As PSObject

# Row 1: 02.04.2026 = April 2nd (not February 4th)
$data[0].Date1.Month | Should -Be 4
$data[0].Date1.Day | Should -Be 2
# Row 1 Date2: 14.04.2026 = April 14th (day > 12, was never ambiguous)
$data[0].Date2.Month | Should -Be 4
$data[0].Date2.Day | Should -Be 14
# Row 2: 11.06.2026 = June 11th (not November 6th)
$data[1].Date1.Month | Should -Be 6
$data[1].Date1.Day | Should -Be 11
# Row 2 Date2: 13.06.2026 = June 13th (day > 12, was never ambiguous)
$data[1].Date2.Month | Should -Be 6
$data[1].Date2.Day | Should -Be 13

Invoke-DbaQuery -SqlInstance $server -Query "DROP TABLE $tableName" -ErrorAction SilentlyContinue
Remove-Item $filePath -ErrorAction SilentlyContinue
}
}

Context "AutoCreateTable post-import optimization" {
Expand Down
Loading