Skip to content

Commit 1e2c718

Browse files
authored
Merge pull request #11 from cajuncoding/feature/add_support_for_bigint_identity_values
Feature/add support for bigint identity values
2 parents 03d8402 + 6958e1f commit 1e2c718

File tree

5 files changed

+120
-47
lines changed

5 files changed

+120
-47
lines changed

NetStandard.SqlBulkHelpers/MaterializedData/MaterializeDataContext.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ public async Task FinishMaterializeDataProcessAsync()
147147
// are valid based on newly materialized data populated in the respective loading tables (for each Live table)!
148148
//1) First Add & Sync all missing FKey constraints (intentionally not added during initial cloning) that will prevent us from being able to switch -- this is still safe within our Transaction!
149149
// NOTE: We could not add FKey constraints at initial load because the links will result in Transaction locks on the Live Tables which we must avoid!!!
150-
// NOTE: WE also disable all FKey constraints on the Live Table so taht ALL FKeys across live/loading/temp are all disabled so that Switching can Occur -- this is still safe within our Transaction!
150+
// NOTE: WE also disable all FKey constraints on the Live Table so that ALL FKeys across live/loading/temp are all disabled so that Switching can Occur -- this is still safe within our Transaction!
151151
foreach (var materializationTableInfo in materializationTables)
152152
{
153153
var fkeyConstraints = materializationTableInfo.LiveTableDefinition.ForeignKeyConstraints.AsArray();
@@ -169,8 +169,6 @@ public async Task FinishMaterializeDataProcessAsync()
169169
.DisableAllTableConstraintChecks(materializationTableInfo.DiscardingTable);
170170
}
171171

172-
var timeoutConvertedToMinutes = Math.Max(1, (int)Math.Ceiling((decimal)BulkHelpersConfig.MaterializeDataStructureProcessingTimeoutSeconds / 60));
173-
174172
//2) Then we can switch all existing Live tables to the Discarding Schema -- this Frees the Live table up to be updated in the next step!
175173
foreach (var materializationTableInfo in materializationTables)
176174
switchScriptBuilder.SwitchTables(
@@ -209,6 +207,8 @@ public async Task FinishMaterializeDataProcessAsync()
209207
.DropTable(materializationTableInfo.DiscardingTable);
210208
}
211209

210+
//var timeoutConvertedToMinutes = Math.Max(1, (int)Math.Ceiling((decimal)BulkHelpersConfig.MaterializeDataStructureProcessingTimeoutSeconds / 60));
211+
212212
await SqlTransaction.ExecuteMaterializedDataSqlScriptAsync(
213213
switchScriptBuilder,
214214
BulkHelpersConfig.MaterializeDataStructureProcessingTimeoutSeconds

NetStandard.SqlBulkHelpers/SqlBulkHelper/BaseSqlBulkHelper.cs

Lines changed: 74 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Linq;
34
using Microsoft.Data.SqlClient;
45
using FastMember;
56
using SqlBulkHelpers.Interfaces;
@@ -9,11 +10,14 @@ namespace SqlBulkHelpers
910
internal static class TypeCache
1011
{
1112
public static readonly Type SqlBulkHelperIdentitySetter = typeof(ISqlBulkHelperIdentitySetter);
13+
public static readonly Type SqlBulkHelperBigIntIdentitySetter = typeof(ISqlBulkHelperBigIntIdentitySetter);
1214
}
1315

1416
//BBernard - Base Class for future flexibility...
1517
internal abstract class BaseSqlBulkHelper<T> : BaseHelper<T> where T : class
1618
{
19+
protected static Type CachedEntityType { get; } = typeof(T);
20+
1721
#region Constructors
1822

1923
/// <inheritdoc/>
@@ -60,7 +64,7 @@ SqlMergeMatchQualifierExpression matchQualifierExpression
6064
protected class MergeResult
6165
{
6266
public int RowNumber { get; set; }
63-
public int IdentityId { get; set; }
67+
public long IdentityId { get; set; }
6468
//public SqlBulkHelpersMergeAction MergeAction { get; set; }
6569
}
6670

@@ -76,6 +80,11 @@ SqlMergeMatchQualifierExpression sqlMatchQualifierExpression
7680

7781
bool uniqueMatchValidationEnabled = sqlMatchQualifierExpression.AssertArgumentIsNotNull(nameof(sqlMatchQualifierExpression)).ThrowExceptionIfNonUniqueMatchesOccur;
7882
bool hasIdentityColumn = identityColumnDefinition != null;
83+
//Small performance improvement here by pre-determining which, if any, identity setters may be implemented by the client;
84+
// since this isn't the most often use case it's helpful to more efficiently skip the type checks while processing...
85+
bool implementsIntIdentitySetter = TypeCache.SqlBulkHelperIdentitySetter.IsAssignableFrom(CachedEntityType);
86+
bool implementsBigIntIdentitySetter = TypeCache.SqlBulkHelperBigIntIdentitySetter.IsAssignableFrom(CachedEntityType);
87+
bool identitySetterInterfaceSupported = implementsIntIdentitySetter || implementsBigIntIdentitySetter;
7988

8089
//If there was no Identity Column or the validation of Unique Merge actions was disabled then we can
8190
// short circuit the post-processing of results as there is nothing to do...
@@ -89,10 +98,7 @@ SqlMergeMatchQualifierExpression sqlMatchQualifierExpression
8998
// we attempt to use Reflection to set the value...
9099
string identityPropertyName = null;
91100

92-
Type entityType = typeof(T);
93-
TypeAccessor fastTypeAccessor = TypeAccessor.Create(entityType);
94-
95-
if (hasIdentityColumn && !TypeCache.SqlBulkHelperIdentitySetter.IsAssignableFrom(entityType))
101+
if (hasIdentityColumn && !identitySetterInterfaceSupported)
96102
{
97103
var processingDefinition = SqlBulkHelpersProcessingDefinition.GetProcessingDefinition<T>(identityColumnDefinition);
98104
identityPropertyName = processingDefinition.IdentityPropDefinition?.PropertyName;
@@ -108,9 +114,11 @@ SqlMergeMatchQualifierExpression sqlMatchQualifierExpression
108114
// so there's no reason to filter the merge results anymore; this is more performant.
109115
var uniqueMatchesHashSet = new HashSet<int>();
110116

111-
var entityResultsList = new List<T>();
117+
var identitySetterAction = hasIdentityColumn
118+
? ResolveIdentitySetterAction(entityList, identityPropertyName)
119+
: null;
112120

113-
//foreach (var mergeResult in mergeResultsList.Where(r => r.MergeAction.HasFlag(SqlBulkHelpersMergeAction.Insert)))
121+
var entityResultsList = new List<T>();
114122
foreach (var mergeResult in mergeResultsList)
115123
{
116124
//ONLY Process uniqueness validation if necessary... otherwise skip the logic altogether.
@@ -135,20 +143,7 @@ SqlMergeMatchQualifierExpression sqlMatchQualifierExpression
135143
//ONLY Process Identity value updates if appropriate... otherwise skip the logic altogether.
136144
//NOTE: List is 0 (zero) based, but our RowNumber is 1 (one) based.
137145
var entity = entityList[mergeResult.RowNumber - 1];
138-
if (hasIdentityColumn)
139-
{
140-
//BBernard
141-
//If the entity supports our interface we can set the value with native performance via the Interface!
142-
if (entity is ISqlBulkHelperIdentitySetter identitySetterEntity)
143-
{
144-
identitySetterEntity.SetIdentityId(mergeResult.IdentityId);
145-
}
146-
else
147-
{
148-
//GENERICALLY Set the Identity Value to the Int value returned, this eliminates any dependency on a Base Class!
149-
fastTypeAccessor[entity, identityPropertyName] = mergeResult.IdentityId;
150-
}
151-
}
146+
identitySetterAction?.Invoke(entity, mergeResult);
152147

153148
entityResultsList.Add(entity);
154149
}
@@ -158,5 +153,63 @@ SqlMergeMatchQualifierExpression sqlMatchQualifierExpression
158153
return entityResultsList;
159154
}
160155

156+
private Action<T, MergeResult> ResolveIdentitySetterAction(List<T> entityList, string identityPropertyName)
157+
{
158+
var sampleEntity = entityList.FirstOrDefault();
159+
switch (sampleEntity)
160+
{
161+
case null:
162+
//Break out to end for Invalid Operation handling...
163+
break;
164+
//BBernard
165+
//For Performance if the entity type supports our Interfaces then we use those to set the Identity ID delegating all logic to the class to handle.
166+
case ISqlBulkHelperIdentitySetter _:
167+
//Downcast our MergeResult long IdentityId to int...
168+
return (entity, mergeResult) => ((ISqlBulkHelperIdentitySetter)entity).SetIdentityId((int)mergeResult.IdentityId);
169+
case ISqlBulkHelperBigIntIdentitySetter _:
170+
return (entity, mergeResult) => ((ISqlBulkHelperBigIntIdentitySetter)entity).SetIdentityId(mergeResult.IdentityId);
171+
default:
172+
{
173+
//Create our TypeAccessor once here, so it can be captured by scope in our Action but is NOT CREATED on each Action execution!
174+
//NOTE: This also helps encapsulate our use of TypeAccessor in case we choose another approach to setting the property in the future!
175+
var fastTypeAccessor = TypeAccessor.Create(CachedEntityType);
176+
var identityPropType = fastTypeAccessor[sampleEntity, identityPropertyName]?.GetType();
177+
if (identityPropType != null)
178+
{
179+
//BBernard
180+
//For Performance we try to identity the primary Integer types and implement the Setter Action with explicit casting, however
181+
// as a fallback we will attempt to generically convert the type via Convert.ChangeType() for really strange edge cases where the Model type is
182+
// something awkward... (Heaven forbid a string), but hey we'll try to make it work.
183+
if (identityPropType == typeof(long)) //BIGINT Sql Type
184+
{
185+
//MergeResult IdentityId is already a Long to support the superset of any other Int property types by down-casting...
186+
return (entity, mergeResult) => fastTypeAccessor[entity, identityPropertyName] = mergeResult.IdentityId;
187+
}
188+
else if (identityPropType == typeof(int)) //INT Sql Type
189+
{
190+
return (entity, mergeResult) => fastTypeAccessor[entity, identityPropertyName] = (int)mergeResult.IdentityId;
191+
}
192+
else if (identityPropType == typeof(short)) //SMALLINT Sql Type
193+
{
194+
return (entity, mergeResult) => fastTypeAccessor[entity, identityPropertyName] = (short)mergeResult.IdentityId;
195+
}
196+
else if (identityPropType == typeof(byte)) //TINYINT Sql Type
197+
{
198+
return (entity, mergeResult) => fastTypeAccessor[entity, identityPropertyName] = (byte)mergeResult.IdentityId;
199+
}
200+
else //For NUMERIC(X, 0) Sql Type or any other, we attempt to generically change the type to match...
201+
{
202+
return (entity, mergeResult) => fastTypeAccessor[entity, identityPropertyName] = Convert.ChangeType(mergeResult.IdentityId, identityPropType);
203+
}
204+
}
205+
206+
//Break out to end for Invalid Operation handling...
207+
break;
208+
}
209+
}
210+
211+
return (entity, mergeResult) => throw new InvalidOperationException($"Unable to properly map the Identity value result into the entity Property [{identityPropertyName}] of type [{entity.GetType()}].");
212+
}
213+
161214
}
162215
}
Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1-
namespace SqlBulkHelpers.Interfaces
1+
using System;
2+
3+
namespace SqlBulkHelpers.Interfaces
24
{
35
public interface ISqlBulkHelperIdentitySetter
46
{
57
void SetIdentityId(int id);
68
}
9+
10+
public interface ISqlBulkHelperBigIntIdentitySetter
11+
{
12+
void SetIdentityId(long id);
13+
}
714
}

NetStandard.SqlBulkHelpers/SqlBulkHelper/QueryProcessing/SqlBulkHelpersMergeQueryBuilder.cs

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ public virtual SqlMergeScriptResults BuildSqlMergeScripts(
1919
var tempStagingTableName = $"#SqlBulkHelpers_STAGING_TABLE_{Guid.NewGuid()}";
2020
var tempOutputIdentityTableName = $"#SqlBulkHelpers_OUTPUT_IDENTITY_TABLE_{Guid.NewGuid()}";
2121
var hasIdentityColumn = tableDefinition.IdentityColumn != null;
22-
var identityColumnName = tableDefinition.IdentityColumn?.ColumnName ?? string.Empty;
2322

2423
//Validate the MatchQualifiers that may be specified, and limit to ONLY valid fields of the Table Definition...
2524
//NOTE: We use the parameter argument for Match Qualifier if specified, otherwise we fall-back to to use the Identity Column.
@@ -93,30 +92,41 @@ public virtual SqlMergeScriptResults BuildSqlMergeScripts(
9392
string mergeTempTablesSql, mergeOutputSql;
9493
if (hasIdentityColumn)
9594
{
95+
var identityColumn = tableDefinition.IdentityColumn;
96+
var identityColumnName = identityColumn.ColumnName;
97+
var identityColumnDataType = identityColumn.DataType;
98+
9699
mergeTempTablesSql = $@"
97100
SELECT TOP(0)
98-
-1 as [{identityColumnName}],
101+
[{identityColumnName}] = CONVERT({identityColumnDataType}, -1),
99102
{columnNamesWithoutIdentityCSV},
100-
-1 as [{SqlBulkHelpersConstants.ROWNUMBER_COLUMN_NAME}]
103+
[{SqlBulkHelpersConstants.ROWNUMBER_COLUMN_NAME}] = CONVERT(INT, -1)
101104
INTO [{tempStagingTableName}]
102105
FROM {tableDefinition.TableFullyQualifiedName};
103106
104107
SELECT TOP(0)
105-
CAST(-1 AS int) [{SqlBulkHelpersConstants.ROWNUMBER_COLUMN_NAME}],
106-
CAST(-1 AS int) as [IDENTITY_ID],
107-
CAST('' AS nvarchar(10)) as [MERGE_ACTION]
108+
[{SqlBulkHelpersConstants.ROWNUMBER_COLUMN_NAME}] = CONVERT(INT, -1),
109+
[IDENTITY_ID] = CONVERT({identityColumnDataType}, -1)
110+
--,[MERGE_ACTION] = CONVERT(VARCHAR(10), '') --Removed as Small Performance Improvement since the Action is not used.
108111
INTO [{tempOutputIdentityTableName}];
109112
";
110113

111114
//All results with Identity Values need to be written to the Output so that we can return them efficiently!
112115
mergeOutputSql = $@"
113-
OUTPUT source.[{SqlBulkHelpersConstants.ROWNUMBER_COLUMN_NAME}], INSERTED.[{identityColumnName}], $action
114-
INTO [{tempOutputIdentityTableName}] ([{SqlBulkHelpersConstants.ROWNUMBER_COLUMN_NAME}], [IDENTITY_ID], [MERGE_ACTION]);
116+
OUTPUT
117+
source.[{SqlBulkHelpersConstants.ROWNUMBER_COLUMN_NAME}],
118+
INSERTED.[{identityColumnName}]
119+
--, $action --Removed as Small Performance Improvement since the Action is not used.
120+
INTO [{tempOutputIdentityTableName}] (
121+
[{SqlBulkHelpersConstants.ROWNUMBER_COLUMN_NAME}],
122+
[IDENTITY_ID]
123+
--, [MERGE_ACTION] --Removed as Small Performance Improvement since the Action is not used.
124+
);
115125
116126
SELECT
117-
[{SqlBulkHelpersConstants.ROWNUMBER_COLUMN_NAME}],
118-
[IDENTITY_ID],
119-
[MERGE_ACTION]
127+
[{SqlBulkHelpersConstants.ROWNUMBER_COLUMN_NAME}],
128+
[IDENTITY_ID]
129+
--,[MERGE_ACTION] --Removed as Small Performance Improvement since the Action is not used.
120130
FROM [{tempOutputIdentityTableName}]
121131
ORDER BY [{SqlBulkHelpersConstants.ROWNUMBER_COLUMN_NAME}] ASC, [IDENTITY_ID] ASC;
122132
";
@@ -126,25 +136,30 @@ FROM [{tempOutputIdentityTableName}]
126136
mergeTempTablesSql = $@"
127137
SELECT TOP(0)
128138
{columnNamesWithoutIdentityCSV},
129-
-1 as [{SqlBulkHelpersConstants.ROWNUMBER_COLUMN_NAME}]
139+
[{SqlBulkHelpersConstants.ROWNUMBER_COLUMN_NAME}] = CONVERT(INT, -1)
130140
INTO [{tempStagingTableName}]
131141
FROM {tableDefinition.TableFullyQualifiedName};
132142
133143
SELECT TOP(0)
134-
CAST(-1 AS int) [{SqlBulkHelpersConstants.ROWNUMBER_COLUMN_NAME}],
135-
CAST('' AS nvarchar(10)) as [MERGE_ACTION]
144+
[{SqlBulkHelpersConstants.ROWNUMBER_COLUMN_NAME}] = CONVERT(INT, -1)
145+
--,[MERGE_ACTION] = CONVERT(VARCHAR(10), '') --Removed as Small Performance Improvement since the Action is not used.
136146
INTO [{tempOutputIdentityTableName}];
137147
";
138148

139149
//All actions need to be written to the Output so that we can return all Identity values for new inserts, and/or post-process
140150
// only results that have been actually changed...
141151
mergeOutputSql = $@"
142-
OUTPUT source.[{SqlBulkHelpersConstants.ROWNUMBER_COLUMN_NAME}], $action
143-
INTO [{tempOutputIdentityTableName}] ([{SqlBulkHelpersConstants.ROWNUMBER_COLUMN_NAME}], [MERGE_ACTION]);
152+
OUTPUT
153+
source.[{SqlBulkHelpersConstants.ROWNUMBER_COLUMN_NAME}]
154+
--, $action --Removed as Small Performance Improvement since the Action is not used.
155+
INTO [{tempOutputIdentityTableName}] (
156+
[{SqlBulkHelpersConstants.ROWNUMBER_COLUMN_NAME}]
157+
--, [MERGE_ACTION] --Removed as Small Performance Improvement since the Action is not used.
158+
);
144159
145160
SELECT
146-
[{SqlBulkHelpersConstants.ROWNUMBER_COLUMN_NAME}],
147-
[MERGE_ACTION]
161+
[{SqlBulkHelpersConstants.ROWNUMBER_COLUMN_NAME}]
162+
--,[MERGE_ACTION] --Removed as Small Performance Improvement since the Action is not used.
148163
FROM [{tempOutputIdentityTableName}]
149164
ORDER BY [{SqlBulkHelpersConstants.ROWNUMBER_COLUMN_NAME}] ASC;
150165
";

NetStandard.SqlBulkHelpers/SqlBulkHelpersConfig.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,8 @@ public bool IsSqlBulkTableLockEnabled
111111
get => SqlBulkCopyOptions.HasFlag(SqlBulkCopyOptions.TableLock);
112112
set
113113
{
114-
if(value)
115-
SqlBulkCopyOptions |= SqlBulkCopyOptions.TableLock;
116-
else
117-
SqlBulkCopyOptions &= ~SqlBulkCopyOptions.TableLock;
114+
if(value) SqlBulkCopyOptions |= SqlBulkCopyOptions.TableLock;
115+
else SqlBulkCopyOptions &= ~SqlBulkCopyOptions.TableLock;
118116
}
119117
}
120118

0 commit comments

Comments
 (0)