Skip to content

Commit

Permalink
Merge pull request #12 from cajuncoding/feature/add_support_to_manual…
Browse files Browse the repository at this point in the history
…ly_set_identity_values_on_insert

Feature/add support to manually set identity values on insert
  • Loading branch information
cajuncoding committed May 24, 2023
2 parents 1e2c718 + dfc9e88 commit 5530fcd
Show file tree
Hide file tree
Showing 14 changed files with 802 additions and 219 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ namespace SqlBulkHelpers
{
public static class SqlBulkHelpersCustomExtensions
{
private const int MaxTableNameLength = 116;

public static T AssertArgumentIsNotNull<T>(this T arg, string argName)
{
if (arg == null) throw new ArgumentNullException(argName);
Expand Down Expand Up @@ -78,13 +80,33 @@ public static string TrimTableNameTerm(this string term)
return trimmedTerm;
}

public static string EnforceUnderscoreTableNameTerm(this string term)
=> term?.Replace(" ", "_");

public static string QualifySqlTerm(this string term)
{
return string.IsNullOrWhiteSpace(term)
? null
: $"[{term.TrimTableNameTerm()}]";
}

public static IEnumerable<string> QualifySqlTerms(this IEnumerable<string> terms)
=> terms.Select(t => t.QualifySqlTerm());

public static string MakeTableNameUnique(this string tableNameToMakeUnique, int uniqueTokenLength = 10)
{
if (string.IsNullOrWhiteSpace(tableNameToMakeUnique))
throw new ArgumentNullException(nameof(tableNameToMakeUnique));

var uniqueTokenSuffix = string.Concat("_", IdGenerator.NewId(uniqueTokenLength));
var uniqueName = string.Concat(tableNameToMakeUnique, uniqueTokenSuffix);

if (uniqueName.Length > MaxTableNameLength)
uniqueName = string.Concat(tableNameToMakeUnique.Substring(0, MaxTableNameLength - uniqueTokenSuffix.Length), uniqueTokenSuffix);

return uniqueName;
}

public static string ToCsv(this IEnumerable<string> enumerableList)
=> string.Join(", ", enumerableList);

Expand Down
100 changes: 100 additions & 0 deletions NetStandard.SqlBulkHelpers/MaterializedData/MaterializeDataHelper.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlTypes;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Data.SqlClient;
Expand Down Expand Up @@ -354,6 +356,104 @@ await sqlTransaction
return tableNameTermsList.AsArray();
}

#endregion

#region Table Identity Column API Methods

/// <summary>
/// Retrieve the Current Identity Value for the specified Table
/// </summary>
/// <param name="sqlConnection"></param>
/// <param name="sqlTransaction"></param>
/// <param name="tableNameOverride"></param>
/// <returns></returns>
public async Task<long> GetTableCurrentIdentityValueAsync(SqlConnection sqlConnection, SqlTransaction sqlTransaction = null, string tableNameOverride = null)
{
using (var sqlCmd = CreateGetTableIdentityValueSqlCommand(sqlConnection, sqlTransaction, tableNameOverride))
{
var identityResult = await sqlCmd.ExecuteScalarAsync().ConfigureAwait(false);

if (identityResult == null)
throw new ArgumentException($"The table specified [{GetMappedTableNameTerm(tableNameOverride).FullyQualifiedTableName}] does not contain an Identity column; current identity value is null.");

var currentIdentityValue = Convert.ToInt64(identityResult);
return currentIdentityValue;
}
}

/// <summary>
/// Retrieve the Current Identity Value for the specified Table
/// </summary>
/// <param name="sqlConnection"></param>
/// <param name="sqlTransaction"></param>
/// <param name="tableNameOverride"></param>
/// <returns></returns>
public long GetTableCurrentIdentityValue(SqlConnection sqlConnection, SqlTransaction sqlTransaction = null, string tableNameOverride = null)
{
using (var sqlCmd = CreateGetTableIdentityValueSqlCommand(sqlConnection, sqlTransaction, tableNameOverride))
{
var identityResult = sqlCmd.ExecuteScalar();

if (identityResult == null)
throw new ArgumentException($"The table specified [{GetMappedTableNameTerm(tableNameOverride).FullyQualifiedTableName}] does not contain an Identity column; current identity value is null.");

var currentIdentityValue = Convert.ToInt64(identityResult);
return currentIdentityValue;
}
}

private SqlCommand CreateGetTableIdentityValueSqlCommand(SqlConnection sqlConnection, SqlTransaction sqlTransaction = null, string tableNameOverride = null)
{
var fullyQualifiedTableName = GetMappedTableNameTerm(tableNameOverride).FullyQualifiedTableName;
var sqlCmd = new SqlCommand("SELECT CURRENT_IDENTITY_VALUE = IDENT_CURRENT(@TableName)", sqlConnection, sqlTransaction);
sqlCmd.CommandTimeout = BulkHelpersConfig.MaterializeDataStructureProcessingTimeoutSeconds;
sqlCmd.Parameters.Add("@TableName", SqlDbType.NVarChar).Value = fullyQualifiedTableName;
return sqlCmd;
}

/// <summary>
/// Sets / Re-seeds the Current Identity Value for the specified Table.
/// </summary>
/// <param name="sqlConnection"></param>
/// <param name="newIdentitySeedValue"></param>
/// <param name="sqlTransaction"></param>
/// <param name="tableNameOverride"></param>
/// <returns></returns>
public async Task ReSeedTableIdentityValueAsync(SqlConnection sqlConnection, long newIdentitySeedValue, SqlTransaction sqlTransaction = null, string tableNameOverride = null)
{
using (var sqlCmd = CreateReSeedTableIdentityValueSqlCommand(sqlConnection, newIdentitySeedValue, sqlTransaction, tableNameOverride))
{
await sqlCmd.ExecuteNonQueryAsync().ConfigureAwait(false);
}
}

/// <summary>
/// Sets / Re-seeds the Current Identity Value for the specified Table.
/// </summary>
/// <param name="sqlConnection"></param>
/// <param name="newIdentitySeedValue"></param>
/// <param name="sqlTransaction"></param>
/// <param name="tableNameOverride"></param>
/// <returns></returns>
public void ReSeedTableIdentityValue(SqlConnection sqlConnection, long newIdentitySeedValue, SqlTransaction sqlTransaction = null, string tableNameOverride = null)
{
using (var sqlCmd = CreateReSeedTableIdentityValueSqlCommand(sqlConnection, newIdentitySeedValue, sqlTransaction, tableNameOverride))
{
sqlCmd.ExecuteNonQuery();
}
}

private SqlCommand CreateReSeedTableIdentityValueSqlCommand(SqlConnection sqlConnection, long newIdentitySeedValue, SqlTransaction sqlTransaction = null, string tableNameOverride = null)
{
var fullyQualifiedTableName = GetMappedTableNameTerm(tableNameOverride).FullyQualifiedTableName;
var sqlCmd = new SqlCommand("DBCC CHECKIDENT(@TableName, RESEED, @NewIdentitySeedValue);", sqlConnection, sqlTransaction);
sqlCmd.CommandTimeout = BulkHelpersConfig.MaterializeDataStructureProcessingTimeoutSeconds;
sqlCmd.Parameters.Add("@TableName", SqlDbType.NVarChar).Value = fullyQualifiedTableName;
sqlCmd.Parameters.Add("@NewIdentitySeedValue", SqlDbType.BigInt).Value = newIdentitySeedValue;
return sqlCmd;
}


#endregion

#region Full Text Index API Methods
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,119 @@ internal static async Task ExecuteMaterializedDataSqlScriptAsync(this SqlConnect

#endregion

#region Table Identity Column Helper Methods (e.g. Get/Set Tables Current Identity Value)

public static Task<long> GetTableCurrentIdentityValueAsync(this SqlTransaction sqlTransaction, string tableName, ISqlBulkHelpersConfig bulkHelpersConfig = null)
=> GetTableCurrentIdentityValueAsync<ISkipMappingLookup>(sqlTransaction, tableName, bulkHelpersConfig);

public static Task<long> GetTableCurrentIdentityValueAsync<T>(this SqlTransaction sqlTransaction, string tableNameOverride = null, ISqlBulkHelpersConfig bulkHelpersConfig = null)
where T : class
{
sqlTransaction.AssertArgumentIsNotNull(nameof(sqlTransaction));
return GetTableCurrentIdentityValueInternalAsync<T>(sqlTransaction.Connection, tableNameOverride, sqlTransaction, bulkHelpersConfig);
}

public static Task<long> GetTableCurrentIdentityValueAsync(this SqlConnection sqlConnection, string tableName, ISqlBulkHelpersConfig bulkHelpersConfig = null)
=> GetTableCurrentIdentityValueInternalAsync<ISkipMappingLookup>(sqlConnection, tableName, bulkHelpersConfig: bulkHelpersConfig);

private static async Task<long> GetTableCurrentIdentityValueInternalAsync<T>(
this SqlConnection sqlConnection,
string tableNameOverride = null,
SqlTransaction sqlTransaction = null,
ISqlBulkHelpersConfig bulkHelpersConfig = null
) where T : class
{
sqlConnection.AssertArgumentIsNotNull(nameof(sqlConnection));

var currentIdentityValue = await new MaterializeDataHelper<T>(bulkHelpersConfig)
.GetTableCurrentIdentityValueAsync(sqlConnection, sqlTransaction, tableNameOverride)
.ConfigureAwait(false);

return currentIdentityValue;
}

public static long GetTableCurrentIdentityValue(this SqlTransaction sqlTransaction, string tableName, ISqlBulkHelpersConfig bulkHelpersConfig = null)
=> GetTableCurrentIdentityValue<ISkipMappingLookup>(sqlTransaction, tableName, bulkHelpersConfig);

public static long GetTableCurrentIdentityValue<T>(this SqlTransaction sqlTransaction, string tableNameOverride = null, ISqlBulkHelpersConfig bulkHelpersConfig = null)
where T : class
{
sqlTransaction.AssertArgumentIsNotNull(nameof(sqlTransaction));
return GetTableCurrentIdentityValueInternal<T>(sqlTransaction.Connection, tableNameOverride, sqlTransaction, bulkHelpersConfig);
}

public static long GetTableCurrentIdentityValue(this SqlConnection sqlConnection, string tableName, ISqlBulkHelpersConfig bulkHelpersConfig = null)
=> GetTableCurrentIdentityValueInternal<ISkipMappingLookup>(sqlConnection, tableName, bulkHelpersConfig: bulkHelpersConfig);

private static long GetTableCurrentIdentityValueInternal<T>(
this SqlConnection sqlConnection,
string tableNameOverride = null,
SqlTransaction sqlTransaction = null,
ISqlBulkHelpersConfig bulkHelpersConfig = null
) where T : class
{
sqlConnection.AssertArgumentIsNotNull(nameof(sqlConnection));

var currentIdentityValue = new MaterializeDataHelper<T>(bulkHelpersConfig).GetTableCurrentIdentityValue(sqlConnection, sqlTransaction, tableNameOverride);
return currentIdentityValue;
}

public static Task ReSeedTableIdentityValueAsync(this SqlTransaction sqlTransaction, string tableName, long newIdentitySeedValue, ISqlBulkHelpersConfig bulkHelpersConfig = null)
=> ReSeedTableIdentityValueAsync<ISkipMappingLookup>(sqlTransaction, newIdentitySeedValue, tableName, bulkHelpersConfig);

public static Task ReSeedTableIdentityValueAsync<T>(this SqlTransaction sqlTransaction, long newIdentitySeedValue, string tableNameOverride = null, ISqlBulkHelpersConfig bulkHelpersConfig = null)
where T : class
{
sqlTransaction.AssertArgumentIsNotNull(nameof(sqlTransaction));
return ReSeedTableIdentityValueInternalAsync<T>(sqlTransaction.Connection, newIdentitySeedValue, tableNameOverride, sqlTransaction, bulkHelpersConfig);
}

public static Task ReSeedTableIdentityValueAsync(this SqlConnection sqlConnection, string tableName, long newIdentitySeedValue, ISqlBulkHelpersConfig bulkHelpersConfig = null)
=> ReSeedTableIdentityValueInternalAsync<ISkipMappingLookup>(sqlConnection, newIdentitySeedValue, tableName, bulkHelpersConfig: bulkHelpersConfig);

private static async Task ReSeedTableIdentityValueInternalAsync<T>(
this SqlConnection sqlConnection,
long newIdentitySeedValue,
string tableNameOverride = null,
SqlTransaction sqlTransaction = null,
ISqlBulkHelpersConfig bulkHelpersConfig = null
) where T : class
{
sqlConnection.AssertArgumentIsNotNull(nameof(sqlConnection));

await new MaterializeDataHelper<T>(bulkHelpersConfig)
.ReSeedTableIdentityValueAsync(sqlConnection, newIdentitySeedValue, sqlTransaction, tableNameOverride)
.ConfigureAwait(false);
}


public static void ReSeedTableIdentityValue(this SqlTransaction sqlTransaction, string tableName, long newIdentitySeedValue, ISqlBulkHelpersConfig bulkHelpersConfig = null)
=> ReSeedTableIdentityValueAsync<ISkipMappingLookup>(sqlTransaction, newIdentitySeedValue, tableName, bulkHelpersConfig);

public static void ReSeedTableIdentityValue<T>(this SqlTransaction sqlTransaction, long newIdentitySeedValue, string tableNameOverride = null, ISqlBulkHelpersConfig bulkHelpersConfig = null)
where T : class
{
sqlTransaction.AssertArgumentIsNotNull(nameof(sqlTransaction));
ReSeedTableIdentityValueInternal<T>(sqlTransaction.Connection, newIdentitySeedValue, tableNameOverride, sqlTransaction, bulkHelpersConfig);
}

public static void ReSeedTableIdentityValue(this SqlConnection sqlConnection, string tableName, long newIdentitySeedValue, ISqlBulkHelpersConfig bulkHelpersConfig = null)
=> ReSeedTableIdentityValueInternal<ISkipMappingLookup>(sqlConnection, newIdentitySeedValue, tableName, bulkHelpersConfig: bulkHelpersConfig);

private static void ReSeedTableIdentityValueInternal<T>(
this SqlConnection sqlConnection,
long newIdentitySeedValue,
string tableNameOverride = null,
SqlTransaction sqlTransaction = null,
ISqlBulkHelpersConfig bulkHelpersConfig = null
) where T : class
{
sqlConnection.AssertArgumentIsNotNull(nameof(sqlConnection));
new MaterializeDataHelper<T>(bulkHelpersConfig).ReSeedTableIdentityValue(sqlConnection, newIdentitySeedValue, sqlTransaction, tableNameOverride);
}

#endregion

#region Full Text Index (cannot be altered within a Transaction)...

/// <summary>
Expand Down
14 changes: 9 additions & 5 deletions NetStandard.SqlBulkHelpers/NetStandard.SqlBulkHelpers.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,16 @@
<Description>A library for easy, efficient and high performance bulk insert and update of data, into a Sql Database, from .Net applications. By leveraging the power of the SqlBulkCopy classes with added support for Identity primary key table columns this library provides a greatly simplified interface to process Identity based Entities with Bulk Performance with the wide compatibility of .NetStandard 2.0.</Description>
<PackageTags>sql server database table bulk insert update identity column sqlbulkcopy orm dapper linq2sql materialization materialized data view materialized-data materialized-view sync replication replica readonly</PackageTags>
<PackageReleaseNotes>
- Added additional convenience methods to the `MaterializationContext` to retreive loading table info for models mapped via annotations (ModelType; vs only ordinal or string name).
- Added support to cancel the materialization process via new `MaterializationContext.CancelMaterializationProcess()` method; allows passive cancelling without the need to throw an exception to safely stop the process.
- Fixed small configuration initialization bugs when manually setting the `IsFullTextIndexHandlingEnabled` flag.
- Fixed small bug where default configuration was not being used as the fallback.
- Added support for other Identity column data types including (INT, BIGINT, SMALLINT, &amp; TINYINT); per feature request (https://github.com/cajuncoding/SqlBulkHelpers/issues/10).
- Added support to explicitly set Identity Values (aka SET IDENTITY_INSERT ON) via new `enableIdentityInsert` api parameter.
- Added support to retreive and re-seed (aka set) the current Identity Value on a given table via new apis in the MaterializedData helpers.
- Additional small bug fixes and optimiaztions.

Prior Relese Notes:
- Added additional convenience methods to the `MaterializationContext` to retreive loading table info for models mapped via annotations (ModelType; vs only ordinal or string name).
- Added support to cancel the materialization process via new `MaterializationContext.CancelMaterializationProcess()` method; allows passive cancelling without the need to throw an exception to safely stop the process.
- Fixed small configuration initialization bugs when manually setting the `IsFullTextIndexHandlingEnabled` flag.
- Fixed small bug where default configuration was not being used as the fallback.
- Improve configuration of Timeouts and add support for Default DB Schema Loader timeout setting.
- v2.0 provides a simplified and easier to access API as Extension Methods of the SqlTransaction class; this is a breaking change for Sql Bulk Insert/Update/etc, but shoudl be easy to migrate to!
- v2.0 release also includes the NEW MaterializeData Helpers to make it significantly easier to implement highly efficient loading and publishing of materialized data via Sql Server much easier via an easy C# API.
Expand All @@ -43,7 +47,7 @@
- Added more Integration Tests for Constructors and Connections, as well as the new DB Schema Loader caching implementation.
- Fixed bug in dynamic initialization of SqlBulkHelpersConnectionProvider and SqlBulkHelpersDBSchemaLoader when not using the Default instances that automtically load the connection string from the application configuration setting.
</PackageReleaseNotes>
<Version>2.1</Version>
<Version>2.2</Version>
</PropertyGroup>

<ItemGroup>
Expand Down

0 comments on commit 5530fcd

Please sign in to comment.