Private/duckdb/Write-DuckDBAppender.ps1
|
function Write-DuckDBAppender { [CmdletBinding()] param( [Parameter(Mandatory)] [DuckDB.NET.Data.DuckDBConnection]$Connection, [Parameter(Mandatory)] [string]$TableName, [Parameter(Mandatory)] $Data, [Parameter(Mandatory=$false)] [switch]$SimpleTypesOnly = $false ) # Read column types from schema so we can cast numeric values correctly. # DuckDB.NET's AppendValue reinterprets bytes rather than converting when # the .NET type does not match the column type (e.g. Int64 into a DOUBLE # column yields 7.4e-323 instead of 15). $columnTypes = @{} $schemaCmd = $Connection.CreateCommand() $schemaCmd.CommandText = "DESCRIBE ""$TableName""" $schemaReader = $schemaCmd.ExecuteReader() while ($schemaReader.Read()) { $columnTypes[$schemaReader.GetString(0)] = $schemaReader.GetString(1) } $schemaReader.Close() $schemaCmd.Dispose() $appender = $Connection.CreateAppender($TableName) $propNames = $null # cached once from first row $i = 0 foreach ($row in $Data) { $i++ # Cache property names from first row only if ($null -eq $propNames) { $propNames = @($row.PSObject.Properties.Name) } $appenderRow = $appender.CreateRow() foreach ($name in $propNames) { $val = $row.$name # Normalize integer subtypes to Int64 before any other check, # because DuckDB.NET appender has no Int32 overload and PowerShell # would otherwise fall back to AppendValue(string). if ($val -is [int] -or $val -is [System.Int16] -or $val -is [byte] -or $val -is [uint16] -or $val -is [uint32]) { $val = [long]$val } elseif ($val -is [float]) { $val = [double]$val } # Cast values to the declared column type so DuckDB.NET picks the # correct AppendValue overload. Without this: # [long] → DOUBLE reinterprets raw bytes (15 becomes 7.4e-323) # [bool] → BIGINT throws "Cannot write Boolean to BigInt column" # [long] → VARCHAR throws "Cannot write Int64 to Varchar column" if ($null -ne $val -and $columnTypes.ContainsKey($name)) { $colType = $columnTypes[$name] $isFloat = $colType -eq 'DOUBLE' -or $colType -eq 'FLOAT' -or $colType -eq 'REAL' -or $colType -eq 'FLOAT4' -or $colType -eq 'FLOAT8' $isInt = $colType -eq 'BIGINT' -or $colType -eq 'INTEGER' -or $colType -eq 'HUGEINT' -or $colType -eq 'INT8' -or $colType -eq 'INT4' if ($val -is [bool] -and $colType -ne 'BOOLEAN') { # bool cannot be appended to non-BOOLEAN columns if ($isFloat) { $val = [double][int]$val } elseif ($isInt) { $val = [long][int]$val } else { $val = [string]$val } } elseif ($val -is [long] -and $isFloat) { $val = [double]$val } elseif ($val -is [double] -and $isInt) { $val = [long]$val } elseif ($colType -eq 'VARCHAR' -and ($val -is [long] -or $val -is [double])) { $val = [string]$val } } # Inlined ConvertTo-DuckDBValue if ($null -eq $val) { # AppendValue([DBNull]::Value) has wrong overload resolution on typed # columns (e.g. resolves to AppendValue(bool) for DOUBLE). Use the # dedicated AppendNullValue() method instead. [void]$appenderRow.AppendNullValue() } elseif (-not $SimpleTypesOnly -and ( $val -is [System.Collections.IList] -or $val -is [PSCustomObject] -or $val -is [System.Collections.IDictionary])) { [void]$appenderRow.AppendValue((ConvertTo-Json -InputObject $val -Compress -Depth 10)) } else { [void]$appenderRow.AppendValue($val) } } $appenderRow.EndRow() If ( $i % 10000 -eq 0 ) { Write-Verbose "[$TableName] Appender: Row $i written." } } $appender.Close() $appender.Dispose() Write-Verbose "[$TableName] Appender finished." } |