Skip to content

Commit 21916f8

Browse files
committed
- Added support to load Table Schema for Temp Tables (basic Schema details needed for BulkInsert or Update, etc. to allow Bulk Loading Temp Tables!
- Improved Error message for when custom SQL Merge Match qualifiers are specified but DB Schema may have changed making them invalid or missing from Cached schema.
1 parent cac2c35 commit 21916f8

File tree

8 files changed

+102
-12
lines changed

8 files changed

+102
-12
lines changed

NetStandard.SqlBulkHelpers/Database/SqlBulkHelpersDBSchemaLoader.Async.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,17 @@ protected SqlCommand CreateSchemaQuerySqlCommand(
5858
)
5959
{
6060
var tableSchemaQuerySql = GetTableSchemaSqlQuery(detailLevel);
61+
62+
//Enable Support for Temp Table Schema Loading...
63+
if (tableNameTerm.IsTempTableName)
64+
{
65+
//NOTE: For Temp Table support all references to INFORMATION_SCHEMA must be replaced with tempdb.INFORMATION_SCHEMA
66+
// and DB_NAME() must be changed to 'tempdb', otherwise we dynamically resolve the true Temp Table Name in the Cte...
67+
tableSchemaQuerySql = tableSchemaQuerySql
68+
.Replace("INFORMATION_SCHEMA.", "tempdb.INFORMATION_SCHEMA.")
69+
.Replace("DB_NAME()", "'tempdb'");
70+
}
71+
6172
var sqlCmd = new SqlCommand(tableSchemaQuerySql, sqlConnection, sqlTransaction);
6273

6374
//Configure the timeout for retrieving the Schema details...
@@ -67,6 +78,7 @@ protected SqlCommand CreateSchemaQuerySqlCommand(
6778
var sqlParams = sqlCmd.Parameters;
6879
sqlParams.Add(new SqlParameter("@TableSchema", tableNameTerm.SchemaName));
6980
sqlParams.Add(new SqlParameter("@TableName", tableNameTerm.TableName));
81+
sqlParams.Add(new SqlParameter("@IsTempTable", tableNameTerm.IsTempTableName));
7082
return sqlCmd;
7183
}
7284

@@ -131,7 +143,8 @@ protected async Task<SqlBulkHelpersTableDefinition> GetTableSchemaDefinitionInte
131143
var tableNameTerm = tableName.ParseAsTableNameTerm();
132144
var cacheKey = CreateCacheKeyInternal(tableNameTerm, detailLevel);
133145

134-
if (forceCacheReload)
146+
//NOTE: We also prevent caching of Temp Table schemas that are by definition Transient!
147+
if (forceCacheReload || tableNameTerm.IsTempTableName)
135148
TableDefinitionsCaseInsensitiveLazyCache.TryRemoveAsyncValue(cacheKey);
136149

137150
var tableDefinition = await TableDefinitionsCaseInsensitiveLazyCache.GetOrAddAsync(

NetStandard.SqlBulkHelpers/Database/SqlBulkHelpersDBSchemaLoader.Sync.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ protected SqlBulkHelpersTableDefinition GetTableSchemaDefinitionInternal(
7575
var tableNameTerm = tableName.ParseAsTableNameTerm();
7676
var cacheKey = CreateCacheKeyInternal(tableNameTerm, detailLevel);
7777

78-
if (forceCacheReload)
78+
//NOTE: We also prevent caching of Temp Table schemas that are by definition Transient!
79+
if (forceCacheReload || tableNameTerm.IsTempTableName)
7980
TableDefinitionsCaseInsensitiveLazyCache.TryRemove(cacheKey);
8081

8182
var tableDefinitionResult = TableDefinitionsCaseInsensitiveLazyCache.GetOrAdd(

NetStandard.SqlBulkHelpers/Database/SqlQueries/QueryDBTableSchemaBasicDetailsJson.sql

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
1-
WITH TablesCte AS (
1+
--NOTE: For Temp Table support all references to INFORMATION_SCHEMA must be replaced with tempdb.INFORMATION_SCHEMA
2+
-- and DB_NAME() must be changed to 'tempdb', otherwise we dynamically resolve the true Temp Table Name in the Cte...
3+
WITH TablesCte AS (
24
SELECT TOP (1)
35
TableSchema = t.[TABLE_SCHEMA],
46
TableName = t.[TABLE_NAME],
57
TableCatalog = t.[TABLE_CATALOG],
6-
ObjectId = OBJECT_ID('['+t.TABLE_SCHEMA+'].['+t.TABLE_NAME+']')
8+
ObjectId = OBJECT_ID(CONCAT('[', t.TABLE_CATALOG, '].[', t.TABLE_SCHEMA, '].[', t.TABLE_NAME, ']'))
79
FROM INFORMATION_SCHEMA.TABLES t
810
WHERE
911
t.TABLE_SCHEMA = @TableSchema
10-
AND t.TABLE_NAME = @TableName
1112
AND t.TABLE_CATALOG = DB_NAME()
13+
AND t.TABLE_NAME = CASE
14+
WHEN @IsTempTable = 0 THEN @TableName
15+
ELSE (SELECT TOP (1) t.[name] FROM tempdb.sys.objects t WHERE t.[object_id] = OBJECT_ID(CONCAT(N'tempdb.[', @TableSchema, '].[', @TableName, ']')))
16+
END
1217
)
1318
SELECT
1419
t.TableSchema,

NetStandard.SqlBulkHelpers/Database/SqlQueries/QueryDBTableSchemaExtendedDetailsJson.sql

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
1-
WITH TablesCte AS (
1+
--NOTE: For Temp Table support all references to INFORMATION_SCHEMA must be replaced with tempdb.INFORMATION_SCHEMA
2+
-- and DB_NAME() must be changed to 'tempdb', otherwise we dynamically resolve the true Temp Table Name in the Cte...
3+
WITH TablesCte AS (
24
SELECT TOP (1)
35
TableSchema = t.[TABLE_SCHEMA],
46
TableName = t.[TABLE_NAME],
57
TableCatalog = t.[TABLE_CATALOG],
6-
ObjectId = OBJECT_ID('['+t.TABLE_SCHEMA+'].['+t.TABLE_NAME+']')
8+
ObjectId = OBJECT_ID(CONCAT('[', t.TABLE_CATALOG, '].[', t.TABLE_SCHEMA, '].[', t.TABLE_NAME, ']'))
79
FROM INFORMATION_SCHEMA.TABLES t
810
WHERE
911
t.TABLE_SCHEMA = @TableSchema
10-
AND t.TABLE_NAME = @TableName
1112
AND t.TABLE_CATALOG = DB_NAME()
13+
AND t.TABLE_NAME = CASE
14+
WHEN @IsTempTable = 0 THEN @TableName
15+
ELSE (SELECT TOP (1) t.[name] FROM tempdb.sys.objects t WHERE t.[object_id] = OBJECT_ID(CONCAT(N'tempdb.[', @TableSchema, '].[', @TableName, ']')))
16+
END
1217
)
1318
SELECT
1419
t.TableSchema,

NetStandard.SqlBulkHelpers/Database/TableNameTerm.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public TableNameTerm(string schemaName, string tableName)
2121
public string TableName { get; }
2222
public string TableNameVariable { get; }
2323
public string FullyQualifiedTableName { get; }
24+
public bool IsTempTableName => TableName.StartsWith("#");
2425

2526
public override string ToString() => FullyQualifiedTableName;
2627

NetStandard.SqlBulkHelpers/NetStandard.SqlBulkHelpers.csproj

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,18 @@
88
<PackageLicenseExpression>MIT</PackageLicenseExpression>
99
<Authors>BBernard / CajunCoding</Authors>
1010
<Company>CajunCoding</Company>
11-
<Version>2.4.0</Version>
11+
<Version>2.4.1</Version>
1212
<PackageProjectUrl>https://github.com/cajuncoding/SqlBulkHelpers</PackageProjectUrl>
1313
<RepositoryUrl>https://github.com/cajuncoding/SqlBulkHelpers</RepositoryUrl>
1414
<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>
1515
<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>
1616
<PackageReleaseNotes>
17-
-Added new explicit CopyTableDataAsync() APIs which enable explicit copying of data between two tables on matching columns (automatically detected by column Name and Data Type).
18-
-Added new Materialized Data Configuration value MaterializedDataLoadingTableDataCopyMode to control whether the materialized data process automatically copies data into the Loading Tables after cloning.
19-
This helps to greatly simplify new use cases where data must be merged (and preserved) during the materialization process.
17+
- Added support to load Table Schema for Temp Tables (basic Schema details needed for BulkInsert or Update, etc. to allow Bulk Loading Temp Tables!
18+
- Improved Error message for when custom SQL Merge Match qualifiers are specified but DB Schema may have changed making them invalid or missing from Cached schema.
2019

2120
Prior Relese Notes:
21+
-Added new explicit CopyTableDataAsync() APIs which enable explicit copying of data between two tables on matching columns (automatically detected by column Name and Data Type).
22+
-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.
2223
- Fixed bug with Sql Bulk Insert/Update processing with Model Properties that have mapped database names via mapping attribute (e.g. [SqlBulkColumn("")], [Map("")], [Column("")], etc.).
2324
- Changed default behaviour to no longer clone tables/schema inside a Transaction which creates a full Schema Lock -- as this greatly impacts Schema aware ORMs such as SqlBulkHelpers, RepoDb, etc.
2425
- New separate methods is now added to handle the CleanupMaterializeDataProcessAsync() but must be explicitly called as it is no longer implicitly called with FinishMaterializeDataProcessAsync().

NetStandard.SqlBulkHelpers/SqlBulkHelper/QueryProcessing/SqlBulkHelpersMergeQueryBuilder.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ public virtual SqlMergeScriptResults BuildSqlMergeScripts(
5252
}
5353
}
5454

55+
if (sanitizedQualifierFields.IsNullOrEmpty())
56+
throw new ArgumentException($"The merge match qualifier fields specified [{matchQualifierExpression.MatchQualifierFields.Select(f => f.Name).ToCsv()}] " +
57+
$"are invalid and could not be resolved to valid fields on the target table {tableDefinition.TableFullyQualifiedName}. " +
58+
$"This could be the result of an invalid argument or potentially due to database schema changes that are not reflected in the schema cache; " +
59+
$"you may try bypassing the cache via the bool forceCacheReload parameter or the static SqlBulkHelpersSchemaLoaderCache.ClearCache() method.");
60+
5561
//If we have valid Fields, then we must re-initialize a valid Qualifier Expression parameter with ONLY the valid fields...
5662
sanitizedQualifierExpression = new SqlMergeMatchQualifierExpression(sanitizedQualifierFields)
5763
{

SqlBulkHelpers.Tests/IntegrationTests/SchemaLoadingTests/SchemaLoaderTests.cs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using System;
22
using System.Diagnostics;
33
using Microsoft.Data.SqlClient;
4+
using RepoDb;
5+
using SqlBulkHelpers.CustomExtensions;
46
using SqlBulkHelpers.SqlBulkHelpers;
57

68
namespace SqlBulkHelpers.Tests.IntegrationTests
@@ -22,6 +24,25 @@ public void TestTableDefinitionLoadingBasicDetailsWithTransactionSyncMethod()
2224
AssertTableDefinitionIsValidForTestElementParentTable(tableDefinition, TableSchemaDetailLevel.BasicDetails);
2325
}
2426

27+
[TestMethod]
28+
public void TestTableDefinitionLoadingBasicDetailsForTempTableWithTransactionSyncMethod()
29+
{
30+
using var sqlConn = SqlConnectionHelper.NewConnection();
31+
using var sqlTransaction = sqlConn.BeginTransaction();
32+
33+
var tempTableName = "#TempTableSchemaLoaderTest";
34+
sqlConn.ExecuteNonQuery($@"
35+
CREATE TABLE [{tempTableName}] ([Id] INT NOT NULL PRIMARY KEY);
36+
", transaction: sqlTransaction);
37+
38+
var tableDefinition = sqlTransaction.GetTableSchemaDefinition(
39+
tempTableName,
40+
TableSchemaDetailLevel.BasicDetails
41+
);
42+
43+
AssertTableDefinitionIsValidForTempTable(tempTableName, tableDefinition);
44+
}
45+
2546
[TestMethod]
2647
public void TestTableDefinitionLoadingExtendedDetailsSyncMethods()
2748
{
@@ -49,6 +70,27 @@ public async Task TestTableDefinitionLoadingBasicDetailsWithTransactionAsync()
4970
AssertTableDefinitionIsValidForTestElementParentTable(tableDefinition, TableSchemaDetailLevel.BasicDetails);
5071
}
5172

73+
[TestMethod]
74+
public async Task TestTableDefinitionLoadingBasicDetailsForTempTableWithTransactionAsync()
75+
{
76+
var sqlConnectionProvider = SqlConnectionHelper.GetConnectionProvider();
77+
await using var sqlConn = await sqlConnectionProvider.NewConnectionAsync().ConfigureAwait(false);
78+
await using var sqlTransaction = (SqlTransaction)await sqlConn.BeginTransactionAsync();
79+
80+
var tempTableName = "#TempTableSchemaLoaderTest";
81+
await sqlConn.ExecuteNonQueryAsync($@"
82+
CREATE TABLE [{tempTableName}] ([Id] INT NOT NULL PRIMARY KEY);
83+
", transaction: sqlTransaction);
84+
85+
var tableDefinition = await sqlTransaction.GetTableSchemaDefinitionAsync(
86+
tempTableName,
87+
TableSchemaDetailLevel.BasicDetails
88+
).ConfigureAwait(false);
89+
90+
AssertTableDefinitionIsValidForTempTable(tempTableName, tableDefinition);
91+
}
92+
93+
5294
[TestMethod]
5395
public async Task TestTableDefinitionLoadingExtendedDetailsAsync()
5496
{
@@ -85,6 +127,22 @@ private void AssertTableDefinitionIsValidForTestElementParentTable(SqlBulkHelper
85127
}
86128
}
87129

130+
private void AssertTableDefinitionIsValidForTempTable(string tempTableName, SqlBulkHelpersTableDefinition tableDefinition)
131+
{
132+
Assert.IsNotNull(tableDefinition);
133+
134+
var tableNameTerm = TableNameTerm.From(tempTableName);
135+
Assert.IsTrue(tableNameTerm.IsTempTableName);
136+
Assert.AreEqual(TableSchemaDetailLevel.BasicDetails, tableDefinition.SchemaDetailLevel);
137+
Assert.AreEqual(tableNameTerm.SchemaName, tableDefinition.TableSchema);
138+
//NOTE: Table Names will not match exactly due to internal Hashing of the Temp Name for Session isolation, etc...
139+
//Assert.AreEqual(tableNameTerm.TableName, tableDefinition.TableName);
140+
//Assert.AreEqual(tableNameTerm.FullyQualifiedTableName, tableDefinition.TableFullyQualifiedName);
141+
Assert.AreEqual(1, tableDefinition.TableColumns.Count);
142+
Assert.IsNotNull(tableDefinition.PrimaryKeyConstraint);
143+
Assert.AreEqual(0, tableDefinition.ForeignKeyConstraints.Count);
144+
}
145+
88146
[TestMethod]
89147
public void TestTableDefinitionLoadingAndCachingSyncMethods()
90148
{

0 commit comments

Comments
 (0)