Skip to content

Commit 32ff013

Browse files
Assembly Conflict Handling & Documentation + CSV stuff (#38)
1 parent 53a7116 commit 32ff013

File tree

15 files changed

+2780
-120
lines changed

15 files changed

+2780
-120
lines changed

.github/workflows/test-avoidconflicts.yml

Lines changed: 441 additions & 0 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,19 +60,22 @@ If you need to use both the SqlServer module and dbatools.library in the same se
6060

6161
### Usage
6262

63-
Import the SqlServer module first, then import dbatools.library with `-AvoidConflicts`:
63+
Import the SqlServer module first, then import dbatools.library **directly** with `-AvoidConflicts`:
6464

6565
```powershell
6666
# Import SqlServer module first
6767
Import-Module SqlServer
6868
6969
# Then import dbatools.library with -AvoidConflicts
70+
# IMPORTANT: Use -ArgumentList $true (NOT a hashtable)
7071
Import-Module dbatools.library -ArgumentList $true
7172
```
7273

74+
> **Note:** You must import `dbatools.library` directly, not via the `dbatools` module. The `-ArgumentList` parameter cannot be passed through module dependencies. If you need to use the full `dbatools` module with SqlServer, import `dbatools.library` first with `-ArgumentList $true`, then import `dbatools`.
75+
7376
When `-AvoidConflicts` is enabled, dbatools.library will:
7477
- Check if each assembly is already loaded in the current session
75-
- Skip loading any assemblies that are already present
78+
- Skip loading any assemblies that are already present (including Microsoft.Data.SqlClient)
7679
- Load only the assemblies that are missing
7780

7881
### Examples
@@ -83,6 +86,13 @@ Import-Module SqlServer
8386
Import-Module dbatools.library -ArgumentList $true
8487
```
8588

89+
**Using with the full dbatools module:**
90+
```powershell
91+
Import-Module SqlServer
92+
Import-Module dbatools.library -ArgumentList $true # Load library first with AvoidConflicts
93+
Import-Module dbatools # Then load dbatools (will use already-loaded library)
94+
```
95+
8696
**See what's being skipped with -Verbose:**
8797
```powershell
8898
Import-Module SqlServer
@@ -96,6 +106,18 @@ Import-Module dbatools.library -ArgumentList $true -Verbose
96106
Import-Module dbatools.library
97107
```
98108

109+
### Common Mistakes
110+
111+
**Wrong:** Using a hashtable for ArgumentList
112+
```powershell
113+
Import-Module dbatools.library -ArgumentList @{AvoidConflicts = $true} # This will NOT work
114+
```
115+
116+
**Correct:** Using a boolean value
117+
```powershell
118+
Import-Module dbatools.library -ArgumentList $true # This works correctly
119+
```
120+
99121
### ⚠️ Important: PowerShell Core + Credentials Issue
100122

101123
**If you plan to use SQL Server credentials with PowerShell Core (pwsh), you MUST install to AllUsers scope or grant appropriate permissions.**

dbatools.library.psd1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
#
88
@{
99
# Version number of this module.
10-
ModuleVersion = '2025.12.21'
10+
ModuleVersion = '2025.12.26'
1111

1212
# ID used to uniquely identify this module
1313
GUID = '00b61a37-6c36-40d8-8865-ac0180288c84'

dbatools.library.psm1

Lines changed: 139 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -17,55 +17,26 @@ function Get-DbatoolsLibraryPath {
1717
$script:libraryroot = Get-DbatoolsLibraryPath
1818

1919
if ($PSVersionTable.PSEdition -ne "Core") {
20-
$dir = [System.IO.Path]::Combine($script:libraryroot, "lib")
21-
$dir = ("$dir\").Replace('\', '\\')
22-
2320
if (-not ("Redirector" -as [type])) {
2421
$source = @"
2522
using System;
26-
using System.Linq;
23+
using System.IO;
2724
using System.Reflection;
28-
using System.Text.RegularExpressions;
2925
3026
public class Redirector
3127
{
32-
public Redirector()
28+
private static string _libPath;
29+
30+
public Redirector(string libPath)
3331
{
32+
_libPath = libPath;
3433
this.EventHandler = new ResolveEventHandler(AssemblyResolve);
3534
}
3635
3736
public readonly ResolveEventHandler EventHandler;
3837
39-
protected Assembly AssemblyResolve(object sender, ResolveEventArgs e)
38+
protected static Assembly AssemblyResolve(object sender, ResolveEventArgs e)
4039
{
41-
string[] dlls = {
42-
"System.Memory",
43-
"System.Runtime",
44-
"System.Management.Automation",
45-
"System.Runtime.CompilerServices.Unsafe",
46-
"Microsoft.Bcl.AsyncInterfaces",
47-
"System.Text.Json",
48-
"System.Resources.Extensions",
49-
"Microsoft.SqlServer.ConnectionInfo",
50-
"Microsoft.SqlServer.Smo",
51-
"Microsoft.Identity.Client",
52-
"System.Diagnostics.DiagnosticSource",
53-
"Microsoft.IdentityModel.Abstractions",
54-
"Microsoft.Data.SqlClient",
55-
"Microsoft.SqlServer.Types",
56-
"System.Configuration.ConfigurationManager",
57-
"Microsoft.SqlServer.Management.Sdk.Sfc",
58-
"Microsoft.SqlServer.Management.IntegrationServices",
59-
"Microsoft.SqlServer.Replication",
60-
"Microsoft.SqlServer.Rmo",
61-
"System.Private.CoreLib",
62-
"Azure.Core",
63-
"Azure.Identity",
64-
"Microsoft.Data.Tools.Utilities",
65-
"Microsoft.Data.Tools.Schema.Sql",
66-
"Microsoft.SqlServer.TransactSql.ScriptDom"
67-
};
68-
6940
var requestedName = new AssemblyName(e.Name);
7041
var assemblyName = requestedName.Name;
7142
@@ -86,13 +57,17 @@ if ($PSVersionTable.PSEdition -ne "Core") {
8657
}
8758
}
8859
89-
// Only load from disk if not already loaded and it's in our list
90-
foreach (string dll in dlls)
60+
// Try to load from our lib folder if the file exists
61+
string dllPath = Path.Combine(_libPath, assemblyName + ".dll");
62+
if (File.Exists(dllPath))
9163
{
92-
if (assemblyName == dll)
64+
try
9365
{
94-
string filelocation = "$dir" + dll + ".dll";
95-
return Assembly.LoadFrom(filelocation);
66+
return Assembly.LoadFrom(dllPath);
67+
}
68+
catch
69+
{
70+
// Failed to load, return null to let default resolution continue
9671
}
9772
}
9873
@@ -105,10 +80,90 @@ if ($PSVersionTable.PSEdition -ne "Core") {
10580
}
10681

10782
try {
108-
$redirector = New-Object Redirector
83+
$libPath = [System.IO.Path]::Combine($script:libraryroot, "lib")
84+
$redirector = New-Object Redirector($libPath)
10985
[System.AppDomain]::CurrentDomain.add_AssemblyResolve($redirector.EventHandler)
11086
} catch {
111-
# unsure
87+
Write-Verbose "Could not register Redirector: $_"
88+
}
89+
} else {
90+
# PowerShell Core: Use AssemblyLoadContext.Resolving event for version redirection
91+
# This handles version mismatches when SqlServer module loads different versions of assemblies
92+
# IMPORTANT: Must be implemented in C# because the resolver runs on .NET threads without PowerShell runspaces
93+
$dir = [System.IO.Path]::Combine($script:libraryroot, "lib")
94+
$dir = ("$dir" + [System.IO.Path]::DirectorySeparatorChar).Replace('\', '\\')
95+
96+
if (-not ("CoreRedirector" -as [type])) {
97+
$coreSource = @"
98+
using System;
99+
using System.IO;
100+
using System.Reflection;
101+
using System.Runtime.Loader;
102+
103+
public class CoreRedirector
104+
{
105+
private static string _libPath;
106+
private static bool _registered = false;
107+
108+
public static void Register(string libPath)
109+
{
110+
if (_registered) return;
111+
_libPath = libPath;
112+
AssemblyLoadContext.Default.Resolving += OnResolving;
113+
_registered = true;
114+
}
115+
116+
private static Assembly OnResolving(AssemblyLoadContext context, AssemblyName assemblyName)
117+
{
118+
string name = assemblyName.Name;
119+
120+
// First, check if any version of this assembly is already loaded
121+
// This handles version mismatches (e.g., dbatools.dll requesting ConnectionInfo 17.100.0.0 when 17.200.0.0 is loaded)
122+
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
123+
{
124+
try
125+
{
126+
if (assembly.GetName().Name == name)
127+
{
128+
return assembly;
129+
}
130+
}
131+
catch
132+
{
133+
// Some assemblies may throw when accessing GetName()
134+
}
135+
}
136+
137+
// Try to load from our lib folder if the file exists
138+
string dllPath = _libPath + name + ".dll";
139+
if (File.Exists(dllPath))
140+
{
141+
try
142+
{
143+
return AssemblyLoadContext.Default.LoadFromAssemblyPath(dllPath);
144+
}
145+
catch
146+
{
147+
// Failed to load, return null to let default resolution continue
148+
}
149+
}
150+
151+
return null;
152+
}
153+
}
154+
"@
155+
156+
try {
157+
$null = Add-Type -TypeDefinition $coreSource -ReferencedAssemblies 'System.Runtime.Loader'
158+
} catch {
159+
Write-Verbose "Could not compile CoreRedirector: $_"
160+
}
161+
}
162+
163+
try {
164+
[CoreRedirector]::Register($dir)
165+
} catch {
166+
Write-Verbose "Could not register CoreRedirector: $_"
112167
}
113168
}
114169

@@ -118,11 +173,44 @@ $sqlclient = [System.IO.Path]::Combine($script:libraryroot, "lib", "Microsoft.Da
118173
# Get loaded assemblies once for reuse (used for AvoidConflicts checks and later assembly loading)
119174
$script:loadedAssemblies = [System.AppDomain]::CurrentDomain.GetAssemblies()
120175

176+
# Check for incompatible System.ClientModel or SqlClient versions
177+
# Azure.Core 1.44+ requires System.ClientModel 1.1+ with IPersistableModel.Write() method
178+
# SqlServer module's SqlClient 5.x includes older System.ClientModel that's incompatible
179+
$script:hasIncompatibleClientModel = $false
180+
if ($AvoidConflicts) {
181+
# Check if System.ClientModel is already loaded with incompatible version
182+
$existingClientModel = $script:loadedAssemblies | Where-Object { $_.GetName().Name -eq 'System.ClientModel' }
183+
if ($existingClientModel) {
184+
$clientModelVersion = $existingClientModel.GetName().Version
185+
# System.ClientModel 1.1.0+ has the required IPersistableModel interface changes
186+
if ($clientModelVersion -lt [Version]'1.1.0') {
187+
$script:hasIncompatibleClientModel = $true
188+
Write-Verbose "Detected incompatible System.ClientModel version $clientModelVersion - will skip Azure.Core and Azure.Identity to avoid MissingMethodException"
189+
}
190+
}
191+
192+
# Check if SqlServer's older SqlClient is loaded (which bundles incompatible System.ClientModel)
193+
# SqlClient 5.x from SqlServer module uses System.ClientModel 1.0.x
194+
# Our Azure.Core requires System.ClientModel 1.1+
195+
if (-not $script:hasIncompatibleClientModel) {
196+
$existingSqlClient = $script:loadedAssemblies | Where-Object { $_.GetName().Name -eq 'Microsoft.Data.SqlClient' }
197+
if ($existingSqlClient) {
198+
$sqlClientVersion = $existingSqlClient.GetName().Version
199+
# SqlClient 5.x bundles older System.ClientModel; 6.x bundles compatible versions
200+
if ($sqlClientVersion.Major -lt 6) {
201+
$script:hasIncompatibleClientModel = $true
202+
Write-Verbose "Detected SqlClient $sqlClientVersion (pre-6.0) which uses incompatible System.ClientModel - will skip Azure.Core and Azure.Identity to avoid MissingMethodException"
203+
}
204+
}
205+
}
206+
}
207+
121208
# Check if SqlClient is already loaded when AvoidConflicts is set
122209
$skipSqlClient = $false
123210
if ($AvoidConflicts) {
124-
$skipSqlClient = $script:loadedAssemblies | Where-Object { $_.GetName().Name -eq 'Microsoft.Data.SqlClient' }
125-
if ($skipSqlClient) {
211+
$existingAssembly = $script:loadedAssemblies | Where-Object { $_.GetName().Name -eq 'Microsoft.Data.SqlClient' }
212+
if ($existingAssembly) {
213+
$skipSqlClient = $true
126214
Write-Verbose "Skipping Microsoft.Data.SqlClient.dll - already loaded"
127215
}
128216
}
@@ -210,6 +298,13 @@ foreach ($name in $names) {
210298
continue
211299
}
212300

301+
# Skip Azure.Core and Azure.Identity if System.ClientModel is incompatible
302+
# These assemblies depend on System.ClientModel 1.1+ which has breaking API changes
303+
if ($script:hasIncompatibleClientModel -and $name -in @('Azure.Core', 'Azure.Identity')) {
304+
Write-Verbose "Skipping $name.dll - incompatible System.ClientModel already loaded"
305+
continue
306+
}
307+
213308
# Check if assembly is already loaded (always check to avoid duplicate loads)
214309
if ($script:loadedAssemblyNames.Contains("$name,")) {
215310
if ($AvoidConflicts) {

project/Dataplat.Dbatools.Csv/CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [1.1.10] - 2025-12-26
11+
12+
### Added
13+
- **SQL Server schema inference** - New `CsvSchemaInference` class that analyzes CSV data to determine optimal SQL Server column types. Two modes available:
14+
- `InferSchemaFromSample()` - Fast inference from first N rows (default 1000)
15+
- `InferSchema()` - Full file scan with progress callback for zero-risk type detection
16+
- `InferredColumn` class containing column name, SQL data type, max length, nullability, unicode flag, and decimal precision/scale
17+
- Type detection for: `uniqueidentifier`, `bit`, `int`, `bigint`, `decimal(p,s)`, `datetime2`, `varchar(n)`, `nvarchar(n)`
18+
- `GenerateCreateTableStatement()` utility to produce SQL DDL from inferred schema
19+
- `ToColumnTypes()` utility to convert inferred schema to `CsvReaderOptions.ColumnTypes` dictionary
20+
- Early exit optimization: types are eliminated as values fail validation, reducing unnecessary checks
21+
- Progress callback support for full-scan mode (fires every ~1% or 10K rows)
22+
- **MoneyConverter** for SQL Server `money`/`smallmoney` types with support for currency symbols, thousands separators, and accounting format
23+
- **VectorConverter** for SQL Server 2025 `VECTOR` data type with support for JSON array and comma-separated formats
24+
25+
### Fixed
26+
- **DecimalConverter scientific notation support** - Changed `NumberStyles` from `Number` to `Float | AllowThousands` to properly parse scientific notation (e.g., `1.2345678E5`)
27+
- **SQL identifier escaping** - Escape closing brackets in schema, table, and column names to prevent SQL injection in generated `CREATE TABLE` statements
28+
1029
## [1.1.1] - 2025-12-04
1130

1231
### Changed

project/Dataplat.Dbatools.Csv/Dataplat.Dbatools.Csv.csproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,20 @@
77

88
<!-- NuGet Package Metadata -->
99
<PackageId>Dataplat.Dbatools.Csv</PackageId>
10-
<Version>1.1.1</Version>
10+
<Version>1.1.10</Version>
1111
<Authors>Chrissy LeMaire</Authors>
1212
<Company>Dataplat</Company>
1313
<Product>Dataplat.Dbatools.Csv</Product>
14-
<Description>High-performance CSV reader and writer for .NET. Features streaming IDataReader for SqlBulkCopy, automatic compression (GZip, Deflate, Brotli, ZLib), multi-character delimiters, parallel processing, string interning, and robust error handling. 20%+ faster than LumenWorks CsvReader. From the trusted dbatools project.</Description>
14+
<Description>High-performance CSV reader with native IDataReader for SqlBulkCopy - 6x faster than legacy solutions for database imports. Database-first design with culture-aware parsing, intelligent null handling, and robust support for messy real-world data (duplicate headers, field mismatches). Features automatic compression (GZip, Deflate, Brotli, ZLib), progress reporting with rows/second metrics, and cancellation support. From the trusted dbatools project.</Description>
1515
<Copyright>Copyright (c) 2025 Chrissy LeMaire</Copyright>
1616
<PackageTags>csv;parser;reader;writer;datareader;idatareader;sqlbulkcopy;compression;gzip;brotli;dbatools;high-performance;parallel</PackageTags>
1717
<PackageLicenseExpression>MIT</PackageLicenseExpression>
18-
<PackageProjectUrl>https://github.com/dataplat/dbatools.library</PackageProjectUrl>
18+
<PackageProjectUrl>https://dataplat.dbatools.io/csv</PackageProjectUrl>
1919
<RepositoryUrl>https://github.com/dataplat/dbatools.library</RepositoryUrl>
2020
<RepositoryType>git</RepositoryType>
2121
<RepositoryBranch>main</RepositoryBranch>
2222
<PackageReadmeFile>README.md</PackageReadmeFile>
23-
<PackageReleaseNotes>Initial release with high-performance CSV parsing, parallel processing support, and comprehensive edge case handling.</PackageReleaseNotes>
23+
<PackageReleaseNotes>v1.1.10: SQL Server schema inference - auto-detect column types (int, bigint, decimal, datetime2, bit, uniqueidentifier, varchar/nvarchar). v1.1.5: Updated package metadata and URL. v1.1.1: ~25% performance improvement for all-columns reads. v1.1.0: Added CancellationToken and progress reporting support.</PackageReleaseNotes>
2424
<PublishRepositoryUrl>true</PublishRepositoryUrl>
2525
<EmbedUntrackedSources>true</EmbedUntrackedSources>
2626
<IncludeSymbols>false</IncludeSymbols>

0 commit comments

Comments
 (0)