Skip to content

Commit dc13cb7

Browse files
Don't cache query type (#1015)
* Don't cache query type * Use tableinfo
1 parent ed6dff4 commit dc13cb7

File tree

3 files changed

+59
-32
lines changed

3 files changed

+59
-32
lines changed

src/SqlAsyncCollector.cs

Lines changed: 31 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ private async Task UpsertRowsAsync(IList<T> rows, SqlAttribute attribute, IConfi
202202
{
203203
TelemetryInstance.TrackEvent(TelemetryEventName.TableInfoCacheMiss, props);
204204
// set the columnNames for supporting T as JObject since it doesn't have columns in the member info.
205-
tableInfo = TableInformation.RetrieveTableInformation(connection, fullTableName, this._logger, GetColumnNamesFromItem(rows.First()), this._serverProperties);
205+
tableInfo = TableInformation.RetrieveTableInformation(connection, fullTableName, this._logger, this._serverProperties);
206206
var policy = new CacheItemPolicy
207207
{
208208
// Re-look up the primary key(s) after timeout (default timeout is 10 minutes)
@@ -244,7 +244,24 @@ private async Task UpsertRowsAsync(IList<T> rows, SqlAttribute attribute, IConfi
244244
}
245245

246246
var table = new SqlObject(fullTableName);
247-
string mergeOrInsertQuery = tableInfo.QueryType == QueryType.Insert ? TableInformation.GetInsertQuery(table, bracketedColumnNamesFromItem) :
247+
248+
IEnumerable<string> objectColumnNames = GetColumnNamesFromItem(rows.First());
249+
IEnumerable<string> primaryKeysFromObject = objectColumnNames.Where(f => tableInfo.PrimaryKeys.Any(k => string.Equals(k.Name, f, StringComparison.Ordinal)));
250+
IEnumerable<PrimaryKey> missingPrimaryKeysFromItem = tableInfo.PrimaryKeys
251+
.Where(k => !primaryKeysFromObject.Contains(k.Name));
252+
// If none of the primary keys are an identity column or have a default value then we require that all primary keys be present in the POCO so we can
253+
// generate the MERGE statement correctly
254+
if (!tableInfo.HasIdentityColumnPrimaryKeys && !tableInfo.HasDefaultColumnPrimaryKeys && missingPrimaryKeysFromItem.Any())
255+
{
256+
string message = $"All primary keys for SQL table {table} need to be found in '{typeof(T)}.' Missing primary keys: [{string.Join(",", missingPrimaryKeysFromItem)}]";
257+
var ex = new InvalidOperationException(message);
258+
TelemetryInstance.TrackException(TelemetryErrorName.MissingPrimaryKeys, ex, connection.AsConnectionProps(this._serverProperties));
259+
throw ex;
260+
}
261+
// If any identity columns or columns with default values aren't included in the object then we have to generate a basic insert since the merge statement expects all primary key
262+
// columns to exist. (the merge statement can handle nullable columns though if those exist)
263+
QueryType queryType = (tableInfo.HasIdentityColumnPrimaryKeys || tableInfo.HasDefaultColumnPrimaryKeys) && missingPrimaryKeysFromItem.Any() ? QueryType.Insert : QueryType.Merge;
264+
string mergeOrInsertQuery = queryType == QueryType.Insert ? TableInformation.GetInsertQuery(table, bracketedColumnNamesFromItem) :
248265
TableInformation.GetMergeQuery(tableInfo.PrimaryKeys, table, bracketedColumnNamesFromItem);
249266

250267
var transactionSw = Stopwatch.StartNew();
@@ -415,28 +432,28 @@ public class TableInformation
415432
public IEnumerable<string> ColumnDefinitions => this.Columns.Select(c => $"{c.Key} {c.Value}");
416433

417434
/// <summary>
418-
/// Whether to use an insert query or merge query.
435+
/// Whether at least one of the primary keys on this table is an identity column
419436
/// </summary>
420-
public QueryType QueryType { get; }
437+
public bool HasIdentityColumnPrimaryKeys { get; }
421438

422439
/// <summary>
423-
/// Whether at least one of the primary keys on this table is an identity column
440+
/// Whether at least one of the primary keys on this table has a default value
424441
/// </summary>
425-
public bool HasIdentityColumnPrimaryKeys { get; }
442+
public bool HasDefaultColumnPrimaryKeys { get; }
443+
426444
/// <summary>
427445
/// Settings to use when serializing the POCO into SQL.
428446
/// Only serialize properties and fields that correspond to SQL columns.
429447
/// </summary>
430448
public JsonSerializerSettings JsonSerializerSettings { get; }
431449

432-
public TableInformation(List<PrimaryKey> primaryKeys, IEnumerable<PropertyInfo> primaryKeyProperties, IDictionary<string, string> columns, QueryType queryType, bool hasIdentityColumnPrimaryKeys)
450+
public TableInformation(List<PrimaryKey> primaryKeys, IEnumerable<PropertyInfo> primaryKeyProperties, IDictionary<string, string> columns, bool hasIdentityColumnPrimaryKeys, bool hasDefaultColumnPrimaryKeys)
433451
{
434452
this.PrimaryKeys = primaryKeys;
435453
this.PrimaryKeyProperties = primaryKeyProperties;
436454
this.Columns = columns;
437-
this.QueryType = queryType;
438455
this.HasIdentityColumnPrimaryKeys = hasIdentityColumnPrimaryKeys;
439-
456+
this.HasDefaultColumnPrimaryKeys = hasDefaultColumnPrimaryKeys;
440457
// Convert datetime strings to ISO 8061 format to avoid potential errors on the server when converting into a datetime. This
441458
// is the only format that are an international standard.
442459
// https://docs.microsoft.com/previous-versions/sql/sql-server-2008-r2/ms180878(v=sql.105)
@@ -546,10 +563,9 @@ WHEN NOT MATCHED THEN
546563
/// <param name="sqlConnection">An open connection with which to query SQL against</param>
547564
/// <param name="fullName">Full name of table, including schema (if exists).</param>
548565
/// <param name="logger">ILogger used to log any errors or warnings.</param>
549-
/// <param name="objectColumnNames">Column names from the object</param>
550566
/// <param name="serverProperties">EngineEdition and Edition of the target Sql Server.</param>
551567
/// <returns>TableInformation object containing primary keys, column types, etc.</returns>
552-
public static TableInformation RetrieveTableInformation(SqlConnection sqlConnection, string fullName, ILogger logger, IEnumerable<string> objectColumnNames, ServerProperties serverProperties)
568+
public static TableInformation RetrieveTableInformation(SqlConnection sqlConnection, string fullName, ILogger logger, ServerProperties serverProperties)
553569
{
554570
Dictionary<TelemetryPropertyName, string> sqlConnProps = sqlConnection.AsConnectionProps(serverProperties);
555571
var table = new SqlObject(fullName);
@@ -627,36 +643,20 @@ public static TableInformation RetrieveTableInformation(SqlConnection sqlConnect
627643

628644
// Match SQL Primary Key column names to POCO property objects. Ensure none are missing.
629645
IEnumerable<PropertyInfo> primaryKeyProperties = typeof(T).GetProperties().Where(f => primaryKeys.Any(k => string.Equals(k.Name, f.Name, StringComparison.Ordinal)));
630-
IEnumerable<string> primaryKeysFromObject = objectColumnNames.Where(f => primaryKeys.Any(k => string.Equals(k.Name, f, StringComparison.Ordinal)));
631-
IEnumerable<PrimaryKey> missingPrimaryKeysFromItem = primaryKeys
632-
.Where(k => !primaryKeysFromObject.Contains(k.Name));
633646
bool hasIdentityColumnPrimaryKeys = primaryKeys.Any(k => k.IsIdentity);
634647
bool hasDefaultColumnPrimaryKeys = primaryKeys.Any(k => k.HasDefault);
635-
// If none of the primary keys are an identity column or have a default value then we require that all primary keys be present in the POCO so we can
636-
// generate the MERGE statement correctly
637-
if (!hasIdentityColumnPrimaryKeys && !hasDefaultColumnPrimaryKeys && missingPrimaryKeysFromItem.Any())
638-
{
639-
string message = $"All primary keys for SQL table {table} need to be found in '{typeof(T)}.' Missing primary keys: [{string.Join(",", missingPrimaryKeysFromItem)}]";
640-
var ex = new InvalidOperationException(message);
641-
TelemetryInstance.TrackException(TelemetryErrorName.MissingPrimaryKeys, ex, sqlConnProps);
642-
throw ex;
643-
}
644-
645-
// If any identity columns or columns with default values aren't included in the object then we have to generate a basic insert since the merge statement expects all primary key
646-
// columns to exist. (the merge statement can handle nullable columns though if those exist)
647-
QueryType queryType = (hasIdentityColumnPrimaryKeys || hasDefaultColumnPrimaryKeys) && missingPrimaryKeysFromItem.Any() ? QueryType.Insert : QueryType.Merge;
648648

649649
tableInfoSw.Stop();
650650
var durations = new Dictionary<TelemetryMeasureName, double>()
651651
{
652652
{ TelemetryMeasureName.GetColumnDefinitionsDurationMs, columnDefinitionsSw.ElapsedMilliseconds },
653653
{ TelemetryMeasureName.GetPrimaryKeysDurationMs, primaryKeysSw.ElapsedMilliseconds }
654654
};
655-
sqlConnProps.Add(TelemetryPropertyName.QueryType, queryType.ToString());
656-
sqlConnProps.Add(TelemetryPropertyName.HasIdentityColumn, hasIdentityColumnPrimaryKeys.ToString());
655+
sqlConnProps.Add(TelemetryPropertyName.HasIdentityColumnPrimaryKeys, hasIdentityColumnPrimaryKeys.ToString());
656+
sqlConnProps.Add(TelemetryPropertyName.HasDefaultColumnPrimaryKeys, hasDefaultColumnPrimaryKeys.ToString());
657657
TelemetryInstance.TrackDuration(TelemetryEventName.GetTableInfo, tableInfoSw.ElapsedMilliseconds, sqlConnProps, durations);
658-
logger.LogDebug($"RetrieveTableInformation DB and Table: {sqlConnection.Database}.{fullName}. Primary keys: [{string.Join(",", primaryKeys.Select(pk => pk.Name))}].\nSQL Column and Definitions: [{string.Join(",", columnDefinitionsFromSQL)}]\nObject columns: [{string.Join(",", objectColumnNames)}]");
659-
return new TableInformation(primaryKeys, primaryKeyProperties, columnDefinitionsFromSQL, queryType, hasIdentityColumnPrimaryKeys);
658+
logger.LogDebug($"RetrieveTableInformation DB and Table: {sqlConnection.Database}.{fullName}. Primary keys: [{string.Join(",", primaryKeys.Select(pk => pk.Name))}].\nSQL Column and Definitions: [{string.Join(",", columnDefinitionsFromSQL)}]");
659+
return new TableInformation(primaryKeys, primaryKeyProperties, columnDefinitionsFromSQL, hasIdentityColumnPrimaryKeys, hasDefaultColumnPrimaryKeys);
660660
}
661661
}
662662

src/Telemetry/Telemetry.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,8 @@ public enum TelemetryPropertyName
358358
{
359359
ErrorCode,
360360
ErrorName,
361-
HasIdentityColumn,
361+
HasIdentityColumnPrimaryKeys,
362+
HasDefaultColumnPrimaryKeys,
362363
HasConfiguredMaxBatchSize,
363364
HasConfiguredMaxChangesPerWorker,
364365
HasConfiguredPollingInterval,

test/Integration/SqlOutputBindingIntegrationTests.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,32 @@ public void AddProductWithDefaultPKTest(SupportedLanguages lang)
354354
Assert.Equal(2, this.ExecuteScalar("SELECT COUNT(*) FROM dbo.ProductsWithDefaultPK"));
355355
}
356356

357+
/// <summary>
358+
/// Regression test for ensuring that the query type isn't cached
359+
/// </summary>
360+
[Theory]
361+
[SqlInlineData()]
362+
public void QueryTypeCachingRegressionTest(SupportedLanguages lang)
363+
{
364+
// Start off by inserting an item into the database, which we'll update later
365+
this.ExecuteNonQuery("INSERT INTO Products VALUES (1, 'test', 100)");
366+
// Now make a call that is expected to fail. The important part here is that:
367+
// 1. This and the function below both target the same table (dbo.Products)
368+
// 2. This one will trigger an "insert" query (which ultimately fails due to the incorrect casing, but the table information is still retrieved first)
369+
Assert.Throws<AggregateException>(() => this.SendOutputGetRequest("addproduct-incorrectcasing", null, TestUtils.GetPort(lang, true)).Wait());
370+
// Ensure that we have the one expected item
371+
Assert.True(1 == (int)this.ExecuteScalar("SELECT COUNT(*) FROM dbo.Products"), "There should be one item initially");
372+
var productWithPrimaryKey = new Dictionary<string, object>()
373+
{
374+
{ "ProductId", 1 },
375+
{ "Name", "MyNewProduct" },
376+
{ "Cost", 100 }
377+
};
378+
// Now send an output request that we expect to succeed - specifically one that will result in an update so requires the MERGE statement
379+
this.SendOutputPostRequest("addproduct", Utils.JsonSerializeObject(productWithPrimaryKey), TestUtils.GetPort(lang)).Wait();
380+
Assert.True(1 == (int)this.ExecuteScalar("SELECT COUNT(*) FROM dbo.Products"), "There should be one item at the end");
381+
}
382+
357383
/// <summary>
358384
/// Tests that when using an unsupported database the expected error is thrown
359385
/// </summary>

0 commit comments

Comments
 (0)