Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
650e8f9
enclosing sql db query executions within a transaction
severussundar Apr 21, 2023
f327c62
catching transaction related exceptions
severussundar Apr 22, 2023
c962eef
using read commited isolation level, adds parallel update test
severussundar Apr 24, 2023
3de120c
updates test to run against book entity
severussundar Apr 24, 2023
834f6e2
Merge branch 'main' into dev/shyamsundarj/transactions-support
severussundar Apr 24, 2023
c5188ad
removing transactionscope for select query execution
severussundar Apr 25, 2023
84ca839
adds concurrent tests
severussundar Apr 25, 2023
7da0ef3
fix formatting
severussundar Apr 25, 2023
844346a
adds helper method to throw exceptions, comments
severussundar Apr 25, 2023
a40571c
Merge branch 'main' into dev/shyamsundarj/transactions-support
severussundar Apr 25, 2023
321081e
updating comments
severussundar Apr 25, 2023
b111b14
Merge branch 'main' into dev/shyamsundarj/transactions-support
severussundar Apr 25, 2023
8a49dc9
adds a helper method to create transaction scope
severussundar Apr 27, 2023
316c237
Merge branch 'main' into dev/shyamsundarj/transactions-support
severussundar Apr 27, 2023
6d1c975
Merge branch 'dev/shyamsundarj/transactions-support' of https://githu…
severussundar Apr 27, 2023
f287248
Merge branch 'main' into dev/shyamsundarj/transactions-support
severussundar May 2, 2023
19701af
uses repeatable read isolation level for mysql
severussundar May 3, 2023
3f65eeb
fix formatting
severussundar May 3, 2023
48ab9ef
Merge branch 'main' into dev/shyamsundarj/transactions-support
severussundar May 3, 2023
b9436b5
resolving merge conflicts
severussundar May 4, 2023
2f3b41b
removes explicit transaction creating using sql stmnts
severussundar May 4, 2023
a99a2b0
updating comment
severussundar May 4, 2023
3fafbb1
Merge branch 'main' into dev/shyamsundarj/transactions-support
severussundar May 5, 2023
be7bb64
Merge branch 'main' into dev/shyamsundarj/transactions-support
severussundar May 8, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,185 @@ public async Task DeleteMutationWithVariablesAndMappings(string dbQuery, string
Assert.AreEqual(result.RootElement.GetProperty("count").GetInt64(), 0);
}

/// <summary>
/// Performs concurrent update mutations on the same item and validates that the responses
/// returned are consistent
/// gQLMutation1 : Updates the title column of Book table to New Title
/// gQLMutation2 : Updates the title column of Book table to Updated Title
/// The title field in the responses returned for each of the mutations should be
/// the same value it had written to the table.
/// </summary>
[TestMethod]
public async Task TestParallelUpdateMutations()
{
string graphQLMutationName = "updatebook";
string gQLMutation1 = @"
mutation {
updatebook(id : 1, item: { title: ""New Title"" })
{
title
}
}";

string gQLMutation2 = @"
mutation {
updatebook(id : 1, item: { title: ""Updated Title"" })
{
title
}
}";

Task<JsonElement> responeTask1 = ExecuteGraphQLRequestAsync(gQLMutation1, graphQLMutationName, isAuthenticated: true);
Task<JsonElement> responseTask2 = ExecuteGraphQLRequestAsync(gQLMutation2, graphQLMutationName, isAuthenticated: true);

JsonElement response1 = await responeTask1;
JsonElement response2 = await responseTask2;

Assert.AreEqual("{\"title\":\"New Title\"}", response1.ToString());
Assert.AreEqual("{\"title\":\"Updated Title\"}", response2.ToString());
}

/// <summary>
/// Performs concurrent insert mutation on a table where the PK is auto-generated.
/// Since, PK is auto-generated, essentially both the mutations are operating on
/// different items. Therefore, both the mutations should succeed.
/// </summary>
[TestMethod]
public async Task TestParallelInsertMutationPKAutoGenerated()
{
string graphQLMutationName = "createbook";
string graphQLMutation1 = @"
mutation {
createbook(item: { title: ""Awesome Book"", publisher_id: 1234 }) {
title
}
}
";

string graphQLMutation2 = @"
mutation {
createbook(item: { title: ""Another Awesome Book"", publisher_id: 1234 }) {
title
}
}
";

Task<JsonElement> responeTask1 = ExecuteGraphQLRequestAsync(graphQLMutation1, graphQLMutationName, isAuthenticated: true);
Task<JsonElement> responseTask2 = ExecuteGraphQLRequestAsync(graphQLMutation2, graphQLMutationName, isAuthenticated: true);

JsonElement response1 = await responeTask1;
JsonElement response2 = await responseTask2;

Assert.AreEqual("{\"title\":\"Awesome Book\"}", response1.ToString());
Assert.AreEqual("{\"title\":\"Another Awesome Book\"}", response2.ToString());

}

/// <summary>
/// Performs concurrent insert mutation on a table where the PK is not auto-generated and
/// validates that only one of the mutations is successful.
/// Both the mutations attempt to create an item with the same primary key. The mutation request
/// that runs first at the database layer should succeed and the other request should fail with
/// primary key violation constraint.
/// </summary>
[TestMethod]
public async Task TestParallelInsertMutationPKNonAutoGenerated()
{
string graphQLMutationName = "createComic";

string graphQLMutation1 = @"
mutation {
createComic (item: { id : 5001, categoryName: ""Fantasy"", title: ""Harry Potter""}){
id
title
}
}
";

string graphQLMutation2 = @"
mutation {
createComic (item: { id : 5001, categoryName: ""Fantasy"", title: ""Lord of the Rings""}){
id
title
}
}
";

Task<JsonElement> responeTask1 = ExecuteGraphQLRequestAsync(graphQLMutation1, graphQLMutationName, isAuthenticated: true);
Task<JsonElement> responseTask2 = ExecuteGraphQLRequestAsync(graphQLMutation2, graphQLMutationName, isAuthenticated: true);

JsonElement response1 = await responeTask1;
JsonElement response2 = await responseTask2;

string responseString1 = response1.ToString();
string responseString2 = response2.ToString();
string expectedStatusCode = $"{DataApiBuilderException.SubStatusCodes.DatabaseOperationFailed}";

// It is not possible to know beforehand which mutation created the new item. So, validations
// are performed for the cases where either mutation could have succeeded. In each case,
// one of the mutation's reponse will contain a valid repsonse and the other mutation's
// response would contain DatabaseOperationFailed sub-status code as it would've failed at
// the database layer due to primary key violation constraint.
if (responseString1.Contains($"\"code\":\"{expectedStatusCode}\""))
{
Assert.AreEqual("{\"id\":5001,\"title\":\"Lord of the Rings\"}", responseString2);
}
else if (responseString2.Contains($"\"code\":\"{expectedStatusCode}\""))
{
Assert.AreEqual("{\"id\":5001,\"title\":\"Harry Potter\"}", responseString1);
}
else
{
Assert.Fail("Unexpected error. Atleast one of the mutations should've succeeded");
}
}

/// <summary>
/// Performs concurrent delete mutations on the same item and validates that only one of the
/// requests is successful.
/// </summary>
[TestMethod]
public async Task TestParallelDeleteMutations()
{
string graphQLMutationName = "deletebook";

string graphQLMutation1 = @"
mutation {
deletebook (id: 1){
id
title
}
}
";

Task<JsonElement> responeTask1 = ExecuteGraphQLRequestAsync(graphQLMutation1, graphQLMutationName, isAuthenticated: true);
Task<JsonElement> responseTask2 = ExecuteGraphQLRequestAsync(graphQLMutation1, graphQLMutationName, isAuthenticated: true);

JsonElement response1 = await responeTask1;
JsonElement response2 = await responseTask2;

string responseString1 = response1.ToString();
string responseString2 = response2.ToString();
string expectedResponse = "{\"id\":1,\"title\":\"Awesome book\"}";

// The mutation request that deletes the item is expected to have a valid response
// and the other mutation is expected to receive an empty response as it
// won't see the item in the table.
if (responseString1.Length == 0)
{
Assert.AreEqual(expectedResponse, responseString2);
}
else if (responseString2.Length == 0)
{
Assert.AreEqual(expectedResponse, responseString1);
}
else
{
Assert.Fail("Unexpected failure. Atleast one of the delete mutations should've succeeded");
}

}

#endregion

#region Negative Tests
Expand Down
20 changes: 5 additions & 15 deletions src/Service/Resolvers/MsSqlQueryBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,7 @@ public string Build(SqlExecuteStructure structure)
$"{BuildProcedureParameterList(structure.ProcedureParameters)}";
}

/// <summary>
/// Avoid redundant check, wrap the sequence in a transaction,
/// and protect the first table access with appropriate locking.
/// </summary>
/// <param name="structure"></param>
/// <returns>Query generated for the PUT(upsert)/PATCH(upsertIncremental) operation.</returns>
/// <inheritdoc />
public string Build(SqlUpsertQueryStructure structure)
{
string tableName = $"{QuoteIdentifier(structure.DatabaseObject.SchemaName)}.{QuoteIdentifier(structure.DatabaseObject.Name)}";
Expand All @@ -125,10 +120,8 @@ public string Build(SqlUpsertQueryStructure structure)
string outputColumns = MakeOutputColumns(structure.OutputColumns, OutputQualifier.Inserted);
string queryToGetCountOfRecordWithPK = $"SELECT COUNT(*) as {COUNT_ROWS_WITH_GIVEN_PK} FROM {tableName} WHERE {pkPredicates}";

// Query to initiate transaction and get number of records with given PK.
string prefixQuery = $"SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;" +
$"BEGIN TRANSACTION;" +
$"DECLARE @ROWS_TO_UPDATE int;" +
// Query to get the number of records with a given PK.
string prefixQuery = $"DECLARE @ROWS_TO_UPDATE int;" +
$"SET @ROWS_TO_UPDATE = ({queryToGetCountOfRecordWithPK}); " +
$"{queryToGetCountOfRecordWithPK};";

Expand All @@ -137,8 +130,8 @@ public string Build(SqlUpsertQueryStructure structure)

// Query to update record (if there exists one for given PK).
StringBuilder updateQuery = new(
$"IF @ROWS_TO_UPDATE = 1" +
$"UPDATE {tableName} WITH(UPDLOCK) " +
$"IF @ROWS_TO_UPDATE = 1 " +
$"UPDATE {tableName} " +
$"SET {updateOperations} " +
$"OUTPUT {outputColumns} " +
$"WHERE {updatePredicates};");
Expand Down Expand Up @@ -172,9 +165,6 @@ public string Build(SqlUpsertQueryStructure structure)
upsertQuery.Append(insertQuery.ToString());
}

// Commit the transaction.
upsertQuery.Append("COMMIT TRANSACTION");

return upsertQuery.ToString();
}

Expand Down
Loading