Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
13 changes: 13 additions & 0 deletions doc/snippets/Microsoft.Data.SqlClient/SqlBulkCopy.xml
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,19 @@ This code is provided to demonstrate the syntax for using **SqlBulkCopy** only.
]]></format>
</example>
</Close>
<InvalidateMetadataCache>
Comment thread
paulmedynski marked this conversation as resolved.
Outdated
<summary>
Clears the cached destination table metadata when using the <see cref="F:Microsoft.Data.SqlClient.SqlBulkCopyOptions.CacheMetadata" /> option.
</summary>
<remarks>
<para>
Call this method when you know the destination table schema has changed and you want to force the next <see cref="M:Microsoft.Data.SqlClient.SqlBulkCopy.WriteToServer(System.Data.Common.DbDataReader)" /> operation to refresh the metadata from the server.
Comment thread
samsharma2700 marked this conversation as resolved.
Outdated
</para>
<para>
The cache is automatically invalidated when the <see cref="P:Microsoft.Data.SqlClient.SqlBulkCopy.DestinationTableName" /> property is changed to a different table name.
Comment thread
samsharma2700 marked this conversation as resolved.
Outdated
</para>
</remarks>
</InvalidateMetadataCache>
<EnableStreaming>
<summary>
Enables or disables a <see cref="T:Microsoft.Data.SqlClient.SqlBulkCopy" /> object to stream data from an <see cref="T:System.Data.IDataReader" /> object
Expand Down
14 changes: 14 additions & 0 deletions doc/snippets/Microsoft.Data.SqlClient/SqlBulkCopyOptions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,19 @@ To see how the option changes the way the bulk load works, run the sample with t
When specified, each batch of the bulk-copy operation will occur within a transaction. If you indicate this option and also provide a <see cref="T:Microsoft.Data.SqlClient.SqlTransaction" /> object to the constructor, an <see cref="T:System.ArgumentException" /> occurs.
</summary>
</UseInternalTransaction>
<CacheMetadata>
<summary>
<para>
When specified, <b>CacheMetadata</b> caches destination table metadata after the first bulk copy operation, allowing subsequent operations to the same table to skip the metadata discovery query. This can improve performance when performing multiple bulk copy operations to the same destination table.
Comment thread
paulmedynski marked this conversation as resolved.
Outdated
</para>
<para>
<b>Warning:</b> Use this option only when you are certain the destination table schema will not change between bulk copy operations. If the table schema changes (columns added, removed, or modified), using cached metadata may result in data corruption, failed operations, or unexpected behavior. Call <see cref="M:Microsoft.Data.SqlClient.SqlBulkCopy.InvalidateMetadataCache" /> to clear the cache if the schema changes.
</para>
<para>
Comment thread
samsharma2700 marked this conversation as resolved.
The cache is automatically invalidated when <see cref="P:Microsoft.Data.SqlClient.SqlBulkCopy.DestinationTableName" /> is changed to a different table.
Changing <see cref="P:Microsoft.Data.SqlClient.SqlBulkCopy.ColumnMappings" /> between operations does not require cache invalidation because the cached metadata describes only the destination table schema, not the source-to-destination column mapping.
Comment thread
samsharma2700 marked this conversation as resolved.
Outdated
</para>
Comment thread
samsharma2700 marked this conversation as resolved.
</summary>
</CacheMetadata>
</members>
</docs>
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,8 @@ public SqlBulkCopy(string connectionString, Microsoft.Data.SqlClient.SqlBulkCopy
public event Microsoft.Data.SqlClient.SqlRowsCopiedEventHandler SqlRowsCopied { add { } remove { } }
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlBulkCopy.xml' path='docs/members[@name="SqlBulkCopy"]/Close/*'/>
public void Close() { }
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlBulkCopy.xml' path='docs/members[@name="SqlBulkCopy"]/InvalidateMetadataCache/*'/>
public void InvalidateMetadataCache() { }
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlBulkCopy.xml' path='docs/members[@name="SqlBulkCopy"]/System.IDisposable.Dispose/*'/>
void System.IDisposable.Dispose() { }
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlBulkCopy.xml' path='docs/members[@name="SqlBulkCopy"]/WriteToServer[@name="DbDataReaderParameter"]/*'/>
Expand Down Expand Up @@ -441,6 +443,8 @@ public enum SqlBulkCopyOptions
{
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlBulkCopyOptions.xml' path='docs/members[@name="SqlBulkCopyOptions"]/AllowEncryptedValueModifications/*'/>
AllowEncryptedValueModifications = 64,
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlBulkCopyOptions.xml' path='docs/members[@name="SqlBulkCopyOptions"]/CacheMetadata/*'/>
CacheMetadata = 128,
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlBulkCopyOptions.xml' path='docs/members[@name="SqlBulkCopyOptions"]/CheckConstraints/*'/>
CheckConstraints = 2,
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlBulkCopyOptions.xml' path='docs/members[@name="SqlBulkCopyOptions"]/Default/*'/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,8 @@ public SqlBulkCopy(string connectionString, Microsoft.Data.SqlClient.SqlBulkCopy
public event Microsoft.Data.SqlClient.SqlRowsCopiedEventHandler SqlRowsCopied { add { } remove { } }
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlBulkCopy.xml' path='docs/members[@name="SqlBulkCopy"]/Close/*'/>
public void Close() { }
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlBulkCopy.xml' path='docs/members[@name="SqlBulkCopy"]/InvalidateMetadataCache/*'/>
public void InvalidateMetadataCache() { }
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlBulkCopy.xml' path='docs/members[@name="SqlBulkCopy"]/System.IDisposable.Dispose/*'/>
void System.IDisposable.Dispose() { }
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlBulkCopy.xml' path='docs/members[@name="SqlBulkCopy"]/WriteToServer[@name="DbDataReaderParameter"]/*'/>
Expand Down Expand Up @@ -359,6 +361,8 @@ public enum SqlBulkCopyOptions
{
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlBulkCopyOptions.xml' path='docs/members[@name="SqlBulkCopyOptions"]/AllowEncryptedValueModifications/*'/>
AllowEncryptedValueModifications = 64,
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlBulkCopyOptions.xml' path='docs/members[@name="SqlBulkCopyOptions"]/CacheMetadata/*'/>
CacheMetadata = 128,
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlBulkCopyOptions.xml' path='docs/members[@name="SqlBulkCopyOptions"]/CheckConstraints/*'/>
CheckConstraints = 2,
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlBulkCopyOptions.xml' path='docs/members[@name="SqlBulkCopyOptions"]/Default/*'/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,10 @@ private int RowNumber

private SourceColumnMetadata[] _currentRowMetadata;

// Metadata caching fields for CacheMetadata option
private BulkCopySimpleResultSet _cachedMetadata;
private string _cachedDestinationTableName;

#if DEBUG
internal static bool s_setAlwaysTaskOnWrite; //when set and in DEBUG mode, TdsParser::WriteBulkCopyValue will always return a task
internal static bool SetAlwaysTaskOnWrite
Expand Down Expand Up @@ -353,6 +357,14 @@ public string DestinationTableName
{
throw ADP.ArgumentOutOfRange(nameof(DestinationTableName));
}

// Invalidate cached metadata if the destination table name changes
if (!string.Equals(_destinationTableName, value, StringComparison.Ordinal))
Comment thread
paulmedynski marked this conversation as resolved.
Outdated
{
_cachedMetadata = null;
Comment thread
paulmedynski marked this conversation as resolved.
Outdated
_cachedDestinationTableName = null;
}

_destinationTableName = value;
}
}
Expand Down Expand Up @@ -497,6 +509,16 @@ IF EXISTS (SELECT TOP 1 * FROM sys.all_columns WHERE [object_id] = OBJECT_ID('sy
// We need to have a _parser.RunAsync to make it real async.
private Task<BulkCopySimpleResultSet> CreateAndExecuteInitialQueryAsync(out BulkCopySimpleResultSet result)
{
// Check if we have valid cached metadata for the current destination table
if (IsCopyOption(SqlBulkCopyOptions.CacheMetadata) &&
Comment thread
samsharma2700 marked this conversation as resolved.
Outdated
_cachedMetadata != null &&
string.Equals(_cachedDestinationTableName, _destinationTableName, StringComparison.Ordinal))
Comment thread
samsharma2700 marked this conversation as resolved.
Outdated
{
SqlClientEventSource.Log.TryTraceEvent("SqlBulkCopy.CreateAndExecuteInitialQueryAsync | Info | Using cached metadata for table '{0}'", _destinationTableName);
result = _cachedMetadata;
return null;
}
Comment thread
samsharma2700 marked this conversation as resolved.
Comment thread
samsharma2700 marked this conversation as resolved.

string TDSCommand = CreateInitialQuery();
SqlClientEventSource.Log.TryTraceEvent("SqlBulkCopy.CreateAndExecuteInitialQueryAsync | Info | Initial Query: '{0}'", TDSCommand);
SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlBulkCopy.CreateAndExecuteInitialQueryAsync | Info | Correlation | Object Id {0}, Activity Id {1}", ObjectID, ActivityCorrelator.Current);
Expand All @@ -506,6 +528,7 @@ private Task<BulkCopySimpleResultSet> CreateAndExecuteInitialQueryAsync(out Bulk
{
result = new BulkCopySimpleResultSet();
RunParser(result);
CacheMetadataIfEnabled(result);
return null;
}
else
Expand All @@ -523,12 +546,23 @@ private Task<BulkCopySimpleResultSet> CreateAndExecuteInitialQueryAsync(out Bulk
{
var internalResult = new BulkCopySimpleResultSet();
RunParserReliably(internalResult);
CacheMetadataIfEnabled(internalResult);
return internalResult;
}
}, TaskScheduler.Default);
}
}

private void CacheMetadataIfEnabled(BulkCopySimpleResultSet result)
{
if (IsCopyOption(SqlBulkCopyOptions.CacheMetadata))
Comment thread
samsharma2700 marked this conversation as resolved.
{
_cachedMetadata = result;
_cachedDestinationTableName = _destinationTableName;
Comment thread
samsharma2700 marked this conversation as resolved.
Outdated
SqlClientEventSource.Log.TryTraceEvent("SqlBulkCopy.CacheMetadataIfEnabled | Info | Cached metadata for table '{0}'", _destinationTableName);
}
}

// Matches associated columns with metadata from initial query.
// Builds and executes the update bulk command.
private string AnalyzeTargetAndCreateUpdateBulkCommand(BulkCopySimpleResultSet internalResults)
Expand Down Expand Up @@ -880,6 +914,14 @@ private void WriteMetaData(BulkCopySimpleResultSet internalResults)
_parser.WriteBulkCopyMetaData(metadataCollection, _sortedColumnMappings.Count, _stateObj);
}

/// <include file='../../../../../../doc/snippets/Microsoft.Data.SqlClient/SqlBulkCopy.xml' path='docs/members[@name="SqlBulkCopy"]/InvalidateMetadataCache/*'/>
public void InvalidateMetadataCache()
Comment thread
paulmedynski marked this conversation as resolved.
Outdated
{
_cachedMetadata = null;
_cachedDestinationTableName = null;
SqlClientEventSource.Log.TryTraceEvent("SqlBulkCopy.InvalidateMetadataCache | Info | Metadata cache invalidated");
}
Comment thread
samsharma2700 marked this conversation as resolved.
Outdated
Comment thread
samsharma2700 marked this conversation as resolved.

// Terminates the bulk copy operation.
// Must be called at the end of the bulk copy session.
/// <include file='../../../../../../doc/snippets/Microsoft.Data.SqlClient/SqlBulkCopy.xml' path='docs/members[@name="SqlBulkCopy"]/Close/*'/>
Expand All @@ -900,6 +942,8 @@ private void Dispose(bool disposing)
// Dispose dependent objects
_columnMappings = null;
_parser = null;
_cachedMetadata = null;
_cachedDestinationTableName = null;
try
{
// Just in case there is a lingering transaction (which there shouldn't be)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ public enum SqlBulkCopyOptions

/// <include file='../../../../../../doc/snippets/Microsoft.Data.SqlClient/SqlBulkCopyOptions.xml' path='docs/members[@name="SqlBulkCopyOptions"]/AllowEncryptedValueModifications/*'/>
AllowEncryptedValueModifications = 1 << 6,

/// <include file='../../../../../../doc/snippets/Microsoft.Data.SqlClient/SqlBulkCopyOptions.xml' path='docs/members[@name="SqlBulkCopyOptions"]/CacheMetadata/*'/>
CacheMetadata = 1 << 7,
Comment thread
samsharma2700 marked this conversation as resolved.
Comment thread
samsharma2700 marked this conversation as resolved.
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using Xunit;

namespace Microsoft.Data.SqlClient.Tests
{
public class SqlBulkCopyCacheMetadataTest
{
[Fact]
public void CacheMetadata_FlagValue_IsCorrect()
{
Assert.Equal(1 << 7, (int)SqlBulkCopyOptions.CacheMetadata);
}

[Fact]
public void CacheMetadata_CanBeCombinedWithOtherOptions()
{
SqlBulkCopyOptions combined =
SqlBulkCopyOptions.CacheMetadata |
SqlBulkCopyOptions.KeepIdentity |
SqlBulkCopyOptions.TableLock;

Assert.True((combined & SqlBulkCopyOptions.CacheMetadata) == SqlBulkCopyOptions.CacheMetadata);
Assert.True((combined & SqlBulkCopyOptions.KeepIdentity) == SqlBulkCopyOptions.KeepIdentity);
Assert.True((combined & SqlBulkCopyOptions.TableLock) == SqlBulkCopyOptions.TableLock);
}

[Fact]
public void CacheMetadata_DoesNotOverlapExistingFlags()
Comment thread
samsharma2700 marked this conversation as resolved.
Outdated
{
int cacheMetadataValue = (int)SqlBulkCopyOptions.CacheMetadata;
Assert.NotEqual((int)SqlBulkCopyOptions.Default, cacheMetadataValue);
Assert.NotEqual((int)SqlBulkCopyOptions.KeepIdentity, cacheMetadataValue);
Assert.NotEqual((int)SqlBulkCopyOptions.CheckConstraints, cacheMetadataValue);
Assert.NotEqual((int)SqlBulkCopyOptions.TableLock, cacheMetadataValue);
Assert.NotEqual((int)SqlBulkCopyOptions.KeepNulls, cacheMetadataValue);
Assert.NotEqual((int)SqlBulkCopyOptions.FireTriggers, cacheMetadataValue);
Assert.NotEqual((int)SqlBulkCopyOptions.UseInternalTransaction, cacheMetadataValue);
Assert.NotEqual((int)SqlBulkCopyOptions.AllowEncryptedValueModifications, cacheMetadataValue);
}

[Fact]
public void InvalidateMetadataCache_CanBeCalledWithoutError()
{
using SqlBulkCopy bulkCopy = new(new SqlConnection());
bulkCopy.InvalidateMetadataCache();
Comment thread
samsharma2700 marked this conversation as resolved.
Outdated
}

[Fact]
public void InvalidateMetadataCache_CanBeCalledMultipleTimes()
{
using SqlBulkCopy bulkCopy = new(new SqlConnection());
bulkCopy.InvalidateMetadataCache();
bulkCopy.InvalidateMetadataCache();
bulkCopy.InvalidateMetadataCache();
}

[Fact]
public void InvalidateMetadataCache_WithCacheMetadataOption()
{
using SqlBulkCopy bulkCopy = new(new SqlConnection(), SqlBulkCopyOptions.CacheMetadata, null);
bulkCopy.InvalidateMetadataCache();
}

[Fact]
public void InvalidateMetadataCache_WithoutCacheMetadataOption()
{
using SqlBulkCopy bulkCopy = new(new SqlConnection(), SqlBulkCopyOptions.Default, null);
bulkCopy.InvalidateMetadataCache();
}

[Fact]
public void DestinationTableName_Change_DoesNotThrowWithCacheMetadata()
{
using SqlBulkCopy bulkCopy = new(new SqlConnection(), SqlBulkCopyOptions.CacheMetadata, null);
bulkCopy.DestinationTableName = "Table1";
bulkCopy.DestinationTableName = "Table2";
bulkCopy.DestinationTableName = "Table1";
}

[Fact]
public void Constructor_WithCacheMetadataOption_Succeeds()
{
using SqlBulkCopy bulkCopy = new(new SqlConnection(), SqlBulkCopyOptions.CacheMetadata, null);
Assert.NotNull(bulkCopy);
}

[Fact]
public void Constructor_WithCacheMetadataAndConnectionString_Succeeds()
{
using SqlBulkCopy bulkCopy = new("Server=localhost", SqlBulkCopyOptions.CacheMetadata);
Assert.NotNull(bulkCopy);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@
<Compile Include="SQL\SqlBulkCopyTest\Bug85007.cs" />
<Compile Include="SQL\SqlBulkCopyTest\Bug903514.cs" />
<Compile Include="SQL\SqlBulkCopyTest\Bug98182.cs" />
<Compile Include="SQL\SqlBulkCopyTest\CacheMetadata.cs" />
<Compile Include="SQL\SqlBulkCopyTest\CheckConstraints.cs" />
<Compile Include="SQL\SqlBulkCopyTest\CopyWidenNullInexactNumerics.cs" />
<Compile Include="SQL\SqlBulkCopyTest\DataConversionErrorMessageTest.cs" />
Expand Down
Loading
Loading