Skip to content

Commit cac2c35

Browse files
committed
-Added new explicit CopyTableDataAsync() APIs which enable explicit copying of data between two tables on matching columns (automatically detected by Name and Data Type).
-Added new Materialized Data Configuration value MaterializedDataLoadingTableDataCopyMode to control whether the materialized data process automatically copies data into the Loading Tables after cloning. This helps to greatly simplify new use cases where data must be merged (and preserved) during the materialization process.
1 parent 0d9de38 commit cac2c35

File tree

7 files changed

+268
-43
lines changed

7 files changed

+268
-43
lines changed

NetStandard.SqlBulkHelpers/MaterializedData/CloneTableInfo.cs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ public readonly struct CloneTableInfo
77
{
88
public TableNameTerm SourceTable { get; }
99
public TableNameTerm TargetTable { get; }
10+
public bool CopyDataFromSource { get; }
1011

11-
public CloneTableInfo(TableNameTerm sourceTable, TableNameTerm? targetTable = null)
12+
public CloneTableInfo(TableNameTerm sourceTable, TableNameTerm? targetTable = null, bool copyDataFromSource = false)
1213
{
1314
sourceTable.AssertArgumentIsNotNull(nameof(sourceTable));
1415

@@ -20,6 +21,7 @@ public CloneTableInfo(TableNameTerm sourceTable, TableNameTerm? targetTable = nu
2021

2122
SourceTable = sourceTable;
2223
TargetTable = validTargetTable;
24+
CopyDataFromSource = copyDataFromSource;
2325
}
2426

2527
/// <summary>
@@ -32,7 +34,7 @@ public CloneTableInfo MakeTargetTableNameUnique()
3234
private static TableNameTerm MakeTableNameUniqueInternal(TableNameTerm tableNameTerm)
3335
=> TableNameTerm.From(tableNameTerm.SchemaName, string.Concat(tableNameTerm.TableName, "_", IdGenerator.NewId(10)));
3436

35-
public static CloneTableInfo From<TSource, TTarget>(string sourceTableName = null, string targetTableName = null, string targetPrefix = null, string targetSuffix = null)
37+
public static CloneTableInfo From<TSource, TTarget>(string sourceTableName = null, string targetTableName = null, string targetPrefix = null, string targetSuffix = null, bool copyDataFromSource = false)
3638
{
3739
//If the generic type is ISkipMappingLookup then we must have a valid sourceTableName specified as a param...
3840
if (SqlBulkHelpersProcessingDefinition.SkipMappingLookupType.IsAssignableFrom(typeof(TSource)))
@@ -55,13 +57,13 @@ public static CloneTableInfo From<TSource, TTarget>(string sourceTableName = nul
5557
}
5658

5759
var targetTable = TableNameTerm.From<TTarget>(targetTableName ?? sourceTableName).ApplyNamePrefixOrSuffix(targetPrefix, targetSuffix);
58-
return new CloneTableInfo(sourceTable, targetTable);
60+
return new CloneTableInfo(sourceTable, targetTable, copyDataFromSource);
5961
}
6062

61-
public static CloneTableInfo From(string sourceTableName, string targetTableName = null, string targetPrefix = null, string targetSuffix = null)
62-
=> From<ISkipMappingLookup, ISkipMappingLookup>(sourceTableName, targetTableName, targetPrefix, targetSuffix);
63+
public static CloneTableInfo From(string sourceTableName, string targetTableName = null, string targetPrefix = null, string targetSuffix = null, bool copyDataFromSource = false)
64+
=> From<ISkipMappingLookup, ISkipMappingLookup>(sourceTableName, targetTableName, targetPrefix, targetSuffix, copyDataFromSource);
6365

64-
public static CloneTableInfo ForNewSchema(TableNameTerm sourceTable, string targetSchemaName, string targetTablePrefix = null, string targetTableSuffix = null)
65-
=> new CloneTableInfo(sourceTable, sourceTable.SwitchSchema(targetSchemaName).ApplyNamePrefixOrSuffix(targetTablePrefix, targetTableSuffix));
66+
public static CloneTableInfo ForNewSchema(TableNameTerm sourceTable, string targetSchemaName, string targetTablePrefix = null, string targetTableSuffix = null, bool copyDataFromSource = false)
67+
=> new CloneTableInfo(sourceTable, sourceTable.SwitchSchema(targetSchemaName).ApplyNamePrefixOrSuffix(targetTablePrefix, targetTableSuffix), copyDataFromSource);
6668
}
6769
}

NetStandard.SqlBulkHelpers/MaterializedData/MaterializeDataHelper.cs

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,9 @@ protected async Task<MaterializationTableInfo[]> CloneTableStructuresForMaterial
163163
originalTableNameTerm,
164164
loadingTablesSchema,
165165
BulkHelpersConfig.MaterializedDataLoadingTablePrefix,
166-
BulkHelpersConfig.MaterializedDataLoadingTableSuffix
166+
BulkHelpersConfig.MaterializedDataLoadingTableSuffix,
167+
//For the Loading Table specify if CopyDataFromSource is enabled based on our configuration...
168+
copyDataFromSource: BulkHelpersConfig.MaterializedDataLoadingTableDataCopyMode == DataCopyMode.CopySourceData
167169
);
168170

169171
//Add Clones for Discarding tables (used for switching Live OUT for later cleanup)...
@@ -208,7 +210,6 @@ await CloneTablesInternalAsync(
208210
sqlTransaction,
209211
cloneInfoToExecuteList,
210212
recreateIfExists: true,
211-
copyDataFromSource: false,
212213
includeFKeyConstraints: false
213214
).ConfigureAwait(false);
214215

@@ -279,14 +280,14 @@ public async Task<CloneTableInfo> CloneTableAsync(
279280
string targetTableName = null,
280281
bool recreateIfExists = false,
281282
bool copyDataFromSource = false
282-
) => (await CloneTablesAsync(sqlTransaction, tablesToClone: new[] { CloneTableInfo.From<T, T>(sourceTableName, targetTableName) }, recreateIfExists).ConfigureAwait(false)).FirstOrDefault();
283+
) => (await CloneTablesAsync(sqlTransaction, tablesToClone: new[] { CloneTableInfo.From<T, T>(sourceTableName, targetTableName) }, recreateIfExists, copyDataFromSource).ConfigureAwait(false)).FirstOrDefault();
283284

284285
public Task<CloneTableInfo[]> CloneTablesAsync(
285286
SqlTransaction sqlTransaction,
286287
bool recreateIfExists,
287288
bool copyDataFromSource,
288289
params CloneTableInfo[] tablesToClone
289-
) => CloneTablesAsync(sqlTransaction, tablesToClone, recreateIfExists, copyDataFromSource);
290+
) => CloneTablesAsync(sqlTransaction, tablesToClone.ToList(), recreateIfExists, copyDataFromSource);
290291

291292
public Task<CloneTableInfo[]> CloneTablesAsync(
292293
SqlTransaction sqlTransaction,
@@ -301,7 +302,6 @@ protected async Task<CloneTableInfo[]> CloneTablesInternalAsync(
301302
SqlTransaction sqlTransaction,
302303
IEnumerable<CloneTableInfo> tablesToClone,
303304
bool recreateIfExists = false,
304-
bool copyDataFromSource = false,
305305
bool includeFKeyConstraints = false
306306
)
307307
{
@@ -334,7 +334,7 @@ protected async Task<CloneTableInfo[]> CloneTablesInternalAsync(
334334
recreateIfExists ? IfExists.Recreate : IfExists.StopProcessingWithException,
335335
cloneIdentitySeedValue: BulkHelpersConfig.IsCloningIdentitySeedValueEnabled,
336336
includeFKeyConstraints: includeFKeyConstraints,
337-
copyDataFromSource: copyDataFromSource
337+
copyDataFromSource: cloneInfo.CopyDataFromSource
338338
);
339339

340340
////TODO: Might (potentially if it doesn't impede performance too much) implement support for re-mapping FKey constraints to Materialization Context tables so data integrity issues will be caught sooner
@@ -356,6 +356,74 @@ await sqlTransaction
356356

357357
#endregion
358358

359+
#region Copy Table Data API Methods
360+
361+
public async Task<CloneTableInfo> CopyTableDataAsync(
362+
SqlTransaction sqlTransaction,
363+
string sourceTableName = null,
364+
string targetTableName = null
365+
) => (await CopyTableDataAsync(sqlTransaction, tablesToProcess: new[] { CloneTableInfo.From<T, T>(sourceTableName, targetTableName, copyDataFromSource: true) }).ConfigureAwait(false)).FirstOrDefault();
366+
367+
public Task<CloneTableInfo[]> CopyTableDataAsync(
368+
SqlTransaction sqlTransaction,
369+
params CloneTableInfo[] tablesToProcess
370+
) => CopyTableDataAsync(sqlTransaction, tablesToProcess.ToList());
371+
372+
public Task<CloneTableInfo[]> CopyTableDataAsync(
373+
SqlTransaction sqlTransaction,
374+
IEnumerable<CloneTableInfo> tablesToProcess
375+
) => CopyTableDataInternalAsync(sqlTransaction, tablesToProcess);
376+
377+
//Internal method with additional flags for normal cloning & materialized data cloning
378+
//NOTE: Materialization process requires special handling such as No FKeys being added to Temp/Loading Tables until ready to Switch
379+
protected async Task<CloneTableInfo[]> CopyTableDataInternalAsync(
380+
SqlTransaction sqlTransaction,
381+
IEnumerable<CloneTableInfo> tablesToClone
382+
)
383+
{
384+
sqlTransaction.AssertArgumentIsNotNull(nameof(sqlTransaction));
385+
386+
var cloneInfoList = tablesToClone.ToList();
387+
388+
if (cloneInfoList.IsNullOrEmpty())
389+
throw new ArgumentException("At least one source & target table pair must be specified.");
390+
391+
var sqlScriptBuilder = MaterializedDataScriptBuilder.NewSqlScript();
392+
var cloneInfoResults = new List<CloneTableInfo>();
393+
foreach (var cloneInfo in cloneInfoList)
394+
{
395+
var sourceTable = cloneInfo.SourceTable;
396+
var targetTable = cloneInfo.TargetTable;
397+
398+
//If both Source & Target are the same (e.g. Target was not explicitly specified) then we adjust
399+
// the Target to ensure we create a copy and append a unique Copy Id...
400+
if (targetTable.EqualsIgnoreCase(sourceTable))
401+
throw new InvalidOperationException($"The source table name {sourceTable.FullyQualifiedTableName} and target table name {targetTable.FullyQualifiedTableName} must be different.");
402+
403+
var sourceTableSchemaDefinition = await GetTableSchemaDefinitionInternalAsync(TableSchemaDetailLevel.BasicDetails, sqlTransaction.Connection, sqlTransaction, sourceTable);
404+
if (sourceTableSchemaDefinition == null)
405+
throw new ArgumentException($"Could not resolve the source table schema for {sourceTable.FullyQualifiedTableName} on the provided connection.");
406+
407+
var targetTableSchemaDefinition = await GetTableSchemaDefinitionInternalAsync(TableSchemaDetailLevel.BasicDetails, sqlTransaction.Connection, sqlTransaction, targetTable);
408+
if (targetTableSchemaDefinition == null)
409+
throw new ArgumentException($"Could not resolve the target table schema for {sourceTable.FullyQualifiedTableName} on the provided connection.");
410+
411+
sqlScriptBuilder.CopyTableData(sourceTableSchemaDefinition, targetTableSchemaDefinition);
412+
413+
cloneInfoResults.Add(new CloneTableInfo(sourceTable, targetTable));
414+
}
415+
416+
//Execute the Script!
417+
await sqlTransaction
418+
.ExecuteMaterializedDataSqlScriptAsync(sqlScriptBuilder, BulkHelpersConfig.MaterializeDataStructureProcessingTimeoutSeconds)
419+
.ConfigureAwait(false);
420+
421+
//If everything was successful then we can simply return the input values as they were all cloned...
422+
return cloneInfoResults.AsArray();
423+
}
424+
425+
#endregion
426+
359427
#region Drop Table API Methods
360428

361429
public async Task<TableNameTerm> DropTableAsync(SqlTransaction sqlTransaction, string tableNameOverride = null)

NetStandard.SqlBulkHelpers/MaterializedData/MaterializedDataScriptBuilder.cs

Lines changed: 54 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ public MaterializedDataScriptBuilder CloneTableWithAllElements(
8989
CloneTableWithColumnsOnly(sourceTableDefinition.TableNameTerm, targetTable, ifExists);
9090

9191
if (copyDataFromSource)
92-
CopyTableData(sourceTableDefinition, targetTable);
92+
CopyTableDataForClone(sourceTableDefinition, targetTable);
9393

9494
//GET the Seed Value immediately, since there is a small chance of it changing...
9595
if (cloneIdentitySeedValue && sourceTableDefinition.IdentityColumn != null)
@@ -106,54 +106,84 @@ public MaterializedDataScriptBuilder CloneTableWithAllElements(
106106
return this;
107107
}
108108

109-
public MaterializedDataScriptBuilder CopyTableData(SqlBulkHelpersTableDefinition sourceTableDefinition, TableNameTerm targetTable)
109+
public MaterializedDataScriptBuilder CopyTableData(SqlBulkHelpersTableDefinition sourceTableDefinition, SqlBulkHelpersTableDefinition targetTableDefinition)
110110
{
111111
sourceTableDefinition.AssertArgumentIsNotNull(nameof(sourceTableDefinition));
112-
targetTable.AssertArgumentIsNotNull(nameof(targetTable));
112+
targetTableDefinition.AssertArgumentIsNotNull(nameof(targetTableDefinition));
113113

114-
bool hasIdentityColumn = sourceTableDefinition.IdentityColumn != null;
114+
var targetColLookup = targetTableDefinition.TableColumns.ToLookup(c => new { c.ColumnName, c.DataType });
115115

116-
//In this overload we handle the Source Table Definition and can dynamically determine if there is An Identity column we handle by enabling insertion for them...
117-
if (hasIdentityColumn)
118-
{
119-
ScriptBuilder.Append($@"
120-
--The Table {sourceTableDefinition.TableFullyQualifiedName} has an Identity Column {sourceTableDefinition.IdentityColumn.ColumnName.QualifySqlTerm()} so we must allow Insertion of IDENTITY values to copy raw table data...
121-
SET IDENTITY_INSERT {targetTable.FullyQualifiedTableName} ON;
122-
");
123-
}
116+
var matchingColumnDefs = sourceTableDefinition.TableColumns
117+
.Where(c => targetColLookup.Contains(new { c.ColumnName, c.DataType }))
118+
.ToArray();
124119

125-
//Now we can Copy data between the two tables...
126-
CopyTableData(sourceTableDefinition.TableNameTerm, targetTable, sourceTableDefinition.TableColumns.AsArray());
120+
if (!matchingColumnDefs.Any())
121+
throw new ArgumentException("There are no matching column definitions between the source & target table schema definitions provided; there must be at least one column matching on name & data type.");
127122

128-
//In this overload we handle the Source Table Definition and can dynamically determine if there is An Identity column we handle by enabling insertion for them...
129-
if (hasIdentityColumn)
130-
{
131-
ScriptBuilder.Append($@"
132-
--We now disable IDENTITY Inserts once all data is copied into {targetTable}...
133-
SET IDENTITY_INSERT {targetTable.FullyQualifiedTableName} OFF;
134-
");
135-
}
123+
//Now we can Copy data between the two tables on the matching columns detected and enable Identity Insert if the
124+
// Target has an Identity Column which might be explicitly copied...
125+
CopyTableData(
126+
sourceTable: sourceTableDefinition.TableNameTerm,
127+
targetTable: targetTableDefinition.TableNameTerm,
128+
enableIdentityInsertOnTarget: targetTableDefinition.IdentityColumn != null,
129+
matchingColumnDefs
130+
);
131+
132+
return this;
133+
}
134+
135+
public MaterializedDataScriptBuilder CopyTableDataForClone(SqlBulkHelpersTableDefinition sourceTableDefinition, TableNameTerm targetTable, params TableColumnDefinition[] columnDefs)
136+
{
137+
sourceTableDefinition.AssertArgumentIsNotNull(nameof(sourceTableDefinition));
138+
targetTable.AssertArgumentIsNotNull(nameof(targetTable));
139+
140+
//Now we can Copy data between the two tables and enable Identity Insert if the Source has an Identity Column (which was cloned)...
141+
CopyTableData(
142+
sourceTableDefinition.TableNameTerm,
143+
targetTable,
144+
enableIdentityInsertOnTarget: sourceTableDefinition.IdentityColumn != null,
145+
sourceTableDefinition.TableColumns.AsArray()
146+
);
136147

137148
return this;
138149
}
139150

140-
public MaterializedDataScriptBuilder CopyTableData(TableNameTerm sourceTable, TableNameTerm targetTable, params TableColumnDefinition[] columnDefs)
151+
public MaterializedDataScriptBuilder CopyTableData(TableNameTerm sourceTable, TableNameTerm targetTable, bool enableIdentityInsertOnTarget, params TableColumnDefinition[] columnDefs)
141152
{
142153
sourceTable.AssertArgumentIsNotNull(nameof(sourceTable));
143154
targetTable.AssertArgumentIsNotNull(nameof(targetTable));
144155

145156
//Validate that we have Table Columns...
146157
if (!columnDefs.HasAny())
147-
throw new ArgumentException($"At least one valid column definition must be specified to denote what data to copy between tables.");
158+
throw new ArgumentException("At least one valid column definition must be specified to denote what data to copy between tables.");
148159

149160
var columnNamesCsv = columnDefs.Select(c => c.ColumnName.QualifySqlTerm()).ToCsv();
150161

162+
//In this overload we handle the Source Table Definition and can dynamically determine if there is An Identity column we handle by enabling insertion for them...
163+
if (enableIdentityInsertOnTarget)
164+
{
165+
ScriptBuilder.Append($@"
166+
--The Table {targetTable.FullyQualifiedTableName} has an Identity Column so we must allow Insertion of IDENTITY values to copy raw table data...
167+
SET IDENTITY_INSERT {targetTable.FullyQualifiedTableName} ON;
168+
");
169+
}
170+
151171
ScriptBuilder.Append($@"
152172
--Syncs the Identity Seed value of the Target Table with the current value of the Source Table (captured into Variable at top of script)
153173
INSERT INTO {targetTable.FullyQualifiedTableName} ({columnNamesCsv})
154174
SELECT {columnNamesCsv}
155175
FROM {sourceTable.FullyQualifiedTableName};
156176
");
177+
178+
//In this overload we handle the Source Table Definition and can dynamically determine if there is An Identity column we handle by enabling insertion for them...
179+
if (enableIdentityInsertOnTarget)
180+
{
181+
ScriptBuilder.Append($@"
182+
--We now disable IDENTITY Inserts once all data is copied into {targetTable.FullyQualifiedTableName}...
183+
SET IDENTITY_INSERT {targetTable.FullyQualifiedTableName} OFF;
184+
");
185+
}
186+
157187
return this;
158188
}
159189

0 commit comments

Comments
 (0)