Skip to content

Commit f5faec7

Browse files
severussundarayush3797seantleonard
authored
Multiple Create - Database Query Generation and Selection Set Resolution (#1994)
## Why make this change? - Closes #1699 This PR tackles the **a)Database Query Generation** and **b)Selection Set Resolution** components of Multiple Mutation/Multiple Create feature. ## What is this change? ### 1. Enhancing the Hotchocolate input parsing logic At the moment, DAB supports create mutation operation on a single entity and a single item. The current parsing logic `BaseSqlQueryStructure.GQLMutArgumentToDictParams()` is written with a foundational idea that a) All the fields present in the create mutation input request belong to the top-level entity b) All the fields will be scalar fields. With the multiple create feature, these no longer hold true. Here, are some items that needs to be accounted for by the parsing logic. a) The fields present in the input request, could correspond to columns of the top-level entity or could be relationship fields b) Relationship fields are not scalar fields. They could be of Object type or List type depending on the type of relationship between top level entity and the related entity. c) Since, nesting levels without any limit is supported, a relationship field can further have a combination of scalar fields + relationship fields. This inner relationship field can again be a combination of scalar fields + relationship fields and so on... d) In addition to point create mutation, a many type mutation operation is also generated. Ex: `createbooks`, `createBookmarks_multiple`. These operations let the user create multiple items of the top-level entity. The root input field for this operation will be `items` as opposed to `item` (for a point create mutation operation). New parsing logic: `SqlMutationEngine.GQLMultipleCreateArgumentToDictParams()` Final data structure of the input params after parsing: Many type Multiple Create: `List<IDictionary<string, object?>>` Point Multiple Create: `IDictionary<string, object?>` ### 2. MultipleCreateStructure - New Wrapper class introduced `MultipleCreateStructure` is used to hold all the relevant information about an entity needed for processing it - determining if there are referencing/referenced entities, determining if there is a linking table insertion necessary along with it, populating the relationship fields in the current entity and linking entity. ### 3. Identifying and handling the implications of a point vs many multiple create operation Logic for identifying and handling different multiple type mutation operations - Point vs Many multiple create operation is added in `SqlMutationEngine.PerformMultipleCreateOperation()` ### 4. Understanding and processing the parsed mutation input request Logic added in `SqlMutationEngine.PerformDbInsertOperation()`. This function handles the core logic from understanding the different fields of the parsed input parameters all the way till the successful creation of records in the database. This function is invoked for both point and many type multiple create operations. The logic in this function at a high-level can be summarized as follows: - Identifying whether the input type is of list type vs object type (indicative of many vs one relationship cardinality) - From the parsed input parameters, identifying the referencing and referenced entities - Identifying and populating the fields belonging to the current and linking table entity. - Recurse for all the referenced entities - the top-level entity depends on the PKs of these entities. Hence, the referenced entities have to processed before the top-level entity. - Once the logic for all the referenced entities have been executed, all the foreign keys needed for the top-level entity should be available. Validate and populate all the foreign keys needed. - Build and execute the database query for creating a new record in the table backing the top-level entity. When building the database query, predicates corresponding to `create` policy (if defined) are added. - Store the PKs of the created record - this is needed for resolving the selection set. - Check if a record has to be created in the linking table. If so, build and execute the database query necessary. - Recurse for all the referencing entities - these entities depend on the PKs of the current entity. Hence, these entities need to processed after the top-level entity. ### 5. Introduction of synchronous methods in QueryExecutor: Given the nature of multiple create feature, the order of execution of code statements is highly important. Consider the example of Book - Publisher which are related through a N:1 relationship. Here, the insertion of Publisher item has to be completed before the logic for populating the foreign keys of Book item can begin to execute. To guarantee the correct order of execution as well maintain the transactional aspects of the feature (successfully rollback all the inserted items if there any failures), the following equivalent synchronous methods were needed. - `ExecuteQuery` - `ExecuteQueryAgainstDb` - `SetManagedIdentityAccessTokenIfAny` - `Read` - `ExtractResultSetFromDbDataReader` - `GetResultProperties` _Note:_ These synchronous methods are used only for the create part of the multiple create feature. The selection set resolution still continues to use the asynchronous equivalent of these methods and takes advantage of the benefits they offer. All the other features continue to use the asynchronous version of these methods. ### 6. Selection Set Resolution - Selection set resolution involves fetching the requested fields on **items created as result of current mutation operation**. The logic for selection set resolution of a point create mutation operation is the same as the one that is present today. However, to resolve the selection set of a many type multiple create operation such as `createbooks` or `createBookmarks_multiple`, new logic had to be introduced. The current limitation with `SqlInsertQueryStructure` is that it can accept PKs of 0 or 1 item. It cannot accept PKs for a list of items which is precisely what is needed for many type multiple create mutation operation. New constructor for `SqlInsertQueryStructure` to accept PKs for a list of items is introduced. - To account for a list of PKs in the eventual `SELECT` statement, the logic for adding predicates is updated. - Logic changes in `SqlQueryStructure.AddPrimaryKeyPredicates()` and `MsSqlQueryBuilder.BuildQueryForMultipleCreateOperation()` _Note:_ Exact database queries for each scenario and query plan analysis are added in the design doc. - For all the entities involved in the selection set, `read` policy will be honored. ### 7. Some more notes about the feature - When the feature flag for multiple create is disabled (or) when used with database types that do not support multiple create operations, a create mutation will follow the current logic of processing a create mutation. Only for databases that support multiple create and when the feature is enabled, this new logic kicks in. _Rationale_: Accounting for related entity fields in every step of processing the mutation request is uncessary. So, when it can be determined that the intention is to not use multiple create feature (either feature flag is disabled or database type does not support the feature), a lot of unnecessary logic can be totally skipped. - **All or None Behavior**: The scope of the transaction applies to the mutation operation as a whole. Irrespective of a point or many type multiple create operation, either all of the items will be created successfully or none of them will be created. ### 8. Introduced new entities and tables for testing Entity Name | Table backing the entity | --- | --- | Publisher_MM | publishers_mm | Book_MM | books_mm | Author_MM | authors_mm | Review_MM | reviews_mm | WebsiteUser_MM | website_users_mm | The intent of introducing these new tables and entities is to define relationships between only through config file and validate different create mutation scenarios. There are no foreign key constraints defined in any of the tables. The relationships defined are only through config file. ### 9. Database Queries executed: Consider the following multiple create mutation request ```graphql mutation multipleCreateExample { createbook( item: { title: "Harry Potter and the Goblet of Fire" publishers: { name: "Bloomsbury" } reviews: [ { content: "Review #1" website_users: { id: 5100, username: "Website User #1" } } { content: "Review #2", websiteuser_id: 1 } ] authors: [ { name: "J.K Rowling" birthdate: "1965-07-31" royalty_percentage: 100.0 } ] } ) { id title publisher_id publishers { id name } reviews { items { book_id id content } } authors { items { id name birthdate } } } } ``` The following database queries are executed in the same order: #### CREATE STATEMENTS ##### 1. Publisher ``` INSERT INTO [dbo].[publishers] ([name]) OUTPUT Inserted.[id] AS [id], Inserted.[name] AS [name] VALUES (@param0); ``` ##### 2. Book ``` INSERT INTO [dbo].[books] ([title], [publisher_id]) OUTPUT Inserted.[id] AS [id], Inserted.[title] AS [title], Inserted.[publisher_id] AS [publisher_id] VALUES (@param0, @param1); ``` ##### 3. Website User ``` INSERT INTO [dbo].[website_users] ([id], [username]) OUTPUT Inserted.[id] AS [id], Inserted.[username] AS [username] VALUES (@param0, @param1); ``` ##### 4. Reviews ``` INSERT INTO [dbo].[reviews] ([book_id], [content], [websiteuser_id]) OUTPUT Inserted.[book_id] AS [book_id], Inserted.[id] AS [id], Inserted.[content] AS [content], Inserted.[websiteuser_id] AS [websiteuser_id] VALUES (@param0, @param1, @Param2); ``` ``` INSERT INTO [dbo].[reviews] ([book_id], [content], [websiteuser_id]) OUTPUT Inserted.[book_id] AS [book_id], Inserted.[id] AS [id], Inserted.[content] AS [content], Inserted.[websiteuser_id] AS [websiteuser_id] VALUES (@param0, @param1, @Param2); ``` ##### 5. Authors ``` INSERT INTO [dbo].[authors] ([name], [birthdate]) OUTPUT Inserted.[id] AS [id], Inserted.[name] AS [name], Inserted.[birthdate] AS [birthdate] VALUES (@param0, @param1); ``` ##### 6. Linking table ``` INSERT INTO [dbo].[book_author_link] ([book_id], [royalty_percentage], [author_id]) OUTPUT Inserted.[book_id] AS [book_id], Inserted.[author_id] AS [author_id], Inserted.[royalty_percentage] AS [royalty_percentage] VALUES (@param0, @param1, @Param2); ``` #### SELECT STATEMENT ``` SELECT TOP 1 [table0].[id] AS [id], [table0].[title] AS [title], [table0].[publisher_id] AS [publisher_id], JSON_QUERY ([table1_subq].[data]) AS [publishers], JSON_QUERY (COALESCE([table4_subq].[data], '[]')) AS [reviews], JSON_QUERY (COALESCE([table8_subq].[data], '[]')) AS [authors] FROM [dbo].[books] AS [table0] OUTER APPLY (SELECT TOP 1 [table1].[id] AS [id], [table1].[name] AS [name] FROM [dbo].[publishers] AS [table1] WHERE [table0].[publisher_id] = [table1].[id] ORDER BY [table1].[id] ASC FOR JSON PATH, INCLUDE_NULL_VALUES,WITHOUT_ARRAY_WRAPPER) AS [table1_subq]([data]) OUTER APPLY (SELECT TOP 100 [table4].[book_id] AS [book_id], [table4].[id] AS [id], [table4].[content] AS [content] FROM [dbo].[reviews] AS [table4] WHERE [table4].[book_id] = [table0].[id] ORDER BY [table4].[book_id] ASC, [table4].[id] ASC FOR JSON PATH, INCLUDE_NULL_VALUES) AS [table4_subq]([data]) OUTER APPLY (SELECT TOP 100 [table8].[id] AS [id], [table8].[name] AS [name], [table8].[birthdate] AS [birthdate] FROM [dbo].[authors] AS [table8] INNER JOIN [dbo].[book_author_link] AS [table12] ON [table12].[author_id] = [table8].[id] WHERE [table12].[book_id] = [table0].[id] ORDER BY [table8].[id] ASC FOR JSON PATH, INCLUDE_NULL_VALUES) AS [table8_subq]([data]) WHERE [table0].[id] = @Param19 ORDER BY [table0].[id] ASC FOR JSON PATH, INCLUDE_NULL_VALUES,WITHOUT_ARRAY_WRAPPER ``` ## How was this tested? - [X] Existing unit tests and integration tests - validate the correct functioning of all other features in DAB - [X] Integration and Unit tests specific to multiple create validate that correctness of multiple create feature. - [X] Manual Tests ## Sample Request(s) ![image](https://github.com/Azure/data-api-builder/assets/11196553/890da495-2c05-42f0-9b65-c936ae4374ad) ![image](https://github.com/Azure/data-api-builder/assets/11196553/7ec84339-736e-405b-8952-c540afbc96e3) --------- Co-authored-by: Ayush Agarwal <ayusha083@gmail.com> Co-authored-by: Ayush Agarwal <34566234+ayush3797@users.noreply.github.com> Co-authored-by: Sean Leonard <sean.leonard@microsoft.com>
1 parent e5cc8de commit f5faec7

30 files changed

+4127
-168
lines changed

config-generators/mssql-commands.txt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
init --config "dab-config.MsSql.json" --database-type mssql --set-session-context true --connection-string "Server=tcp:127.0.0.1,1433;Persist Security Info=False;User ID=sa;Password=REPLACEME;MultipleActiveResultSets=False;Connection Timeout=5;" --host-mode Development --cors-origin "http://localhost:5000" --graphql.multiple-create.enabled true
22
add Publisher --config "dab-config.MsSql.json" --source publishers --permissions "anonymous:read"
3+
add Publisher_MM --config "dab-config.MsSql.json" --source publishers_mm --graphql "Publisher_MM:Publishers_MM" --permissions "anonymous:*"
34
add Stock --config "dab-config.MsSql.json" --source stocks --permissions "anonymous:create,read,update,delete"
45
add Book --config "dab-config.MsSql.json" --source books --permissions "anonymous:create,read,update,delete" --graphql "book:books"
6+
add Book_MM --config "dab-config.MsSql.json" --source books_mm --permissions "anonymous:*" --graphql "book_mm:books_mm"
57
add BookWebsitePlacement --config "dab-config.MsSql.json" --source book_website_placements --permissions "anonymous:read"
68
add Author --config "dab-config.MsSql.json" --source authors --permissions "anonymous:read"
9+
add Author_MM --config "dab-config.MsSql.json" --source authors_mm --graphql "author_mm:authors_mm" --permissions "anonymous:*"
710
add Revenue --config "dab-config.MsSql.json" --source revenues --permissions "anonymous:*"
811
add Review --config "dab-config.MsSql.json" --source reviews --permissions "anonymous:create,read,update" --rest true --graphql "review:reviews"
12+
add Review_MM --config "dab-config.MsSql.json" --source reviews_mm --permissions "anonymous:*" --rest true --graphql "review_mm:reviews_mm"
913
add Comic --config "dab-config.MsSql.json" --source comics --permissions "anonymous:create,read,update"
1014
add Broker --config "dab-config.MsSql.json" --source brokers --permissions "anonymous:read"
1115
add WebsiteUser --config "dab-config.MsSql.json" --source website_users --permissions "anonymous:create,read,delete,update"
16+
add WebsiteUser_MM --config "dab-config.MsSql.json" --source website_users_mm --graphql "websiteuser_mm:websiteusers_mm" --permissions "anonymous:*"
1217
add SupportedType --config "dab-config.MsSql.json" --source type_table --permissions "anonymous:create,read,delete,update"
1318
add stocks_price --config "dab-config.MsSql.json" --source stocks_price --permissions "authenticated:create,read,update,delete"
1419
update stocks_price --config "dab-config.MsSql.json" --permissions "anonymous:read"
@@ -71,6 +76,9 @@ update Publisher --config "dab-config.MsSql.json" --permissions "policy_tester_0
7176
update Publisher --config "dab-config.MsSql.json" --permissions "database_policy_tester:read" --policy-database "@item.id ne 1234 or @item.id gt 1940"
7277
update Publisher --config "dab-config.MsSql.json" --permissions "database_policy_tester:update" --policy-database "@item.id ne 1234"
7378
update Publisher --config "dab-config.MsSql.json" --permissions "database_policy_tester:create" --policy-database "@item.name ne 'New publisher'"
79+
update Publisher --config "dab-config.MsSql.json" --permissions "role_multiple_create_policy_tester:create" --policy-database "@item.name ne 'Test'"
80+
update Publisher --config "dab-config.MsSql.json" --permissions "role_multiple_create_policy_tester:read,update,delete"
81+
update Publisher_MM --config "dab-config.MsSql.json" --permissions "authenticated:*" --relationship books_mm --relationship.fields "id:publisher_id" --target.entity Book_MM --cardinality many
7482
update Stock --config "dab-config.MsSql.json" --permissions "authenticated:create,read,update,delete"
7583
update Stock --config "dab-config.MsSql.json" --permissions "test_role_with_excluded_fields_on_create:read,update,delete"
7684
update Stock --config "dab-config.MsSql.json" --permissions "test_role_with_excluded_fields_on_create:create" --fields.exclude "piecesAvailable"
@@ -112,12 +120,27 @@ update Book --config "dab-config.MsSql.json" --permissions "test_role_with_exclu
112120
update Book --config "dab-config.MsSql.json" --permissions "test_role_with_excluded_fields:read" --fields.exclude "publisher_id"
113121
update Book --config "dab-config.MsSql.json" --permissions "test_role_with_policy_excluded_fields:create,update,delete"
114122
update Book --config "dab-config.MsSql.json" --permissions "test_role_with_policy_excluded_fields:read" --fields.exclude "publisher_id" --policy-database "@item.title ne 'Test'"
123+
update Book --config "dab-config.MsSql.json" --permissions "role_multiple_create_policy_tester:read" --policy-database "@item.publisher_id ne 1234"
124+
update Book --config "dab-config.MsSql.json" --permissions "role_multiple_create_policy_tester:create" --policy-database "@item.title ne 'Test'"
125+
update Book --config "dab-config.MsSql.json" --permissions "role_multiple_create_policy_tester:update,delete"
126+
update Book_MM --config "dab-config.MsSql.json" --permissions "authenticated:*"
127+
update Book_MM --config "dab-config.MsSql.json" --relationship publishers --target.entity Publisher_MM --cardinality one --relationship.fields "publisher_id:id"
128+
update Book_MM --config "dab-config.MsSql.json" --relationship reviews --target.entity Review_MM --cardinality many --relationship.fields "id:book_id"
129+
update Book_MM --config "dab-config.MsSql.json" --relationship authors --relationship.fields "id:id" --target.entity Author_MM --cardinality many --linking.object book_author_link_mm --linking.source.fields "book_id" --linking.target.fields "author_id"
115130
update Review --config "dab-config.MsSql.json" --permissions "authenticated:create,read,update,delete"
116131
update Review --config "dab-config.MsSql.json" --relationship books --target.entity Book --cardinality one
132+
update Review --config "dab-config.MsSql.json" --relationship website_users --target.entity WebsiteUser --cardinality one --relationship.fields "websiteuser_id:id"
133+
update Review --config "dab-config.MsSql.json" --permissions "role_multiple_create_policy_tester:read" --policy-database "@item.websiteuser_id ne 1"
134+
update Review --config "dab-config.MsSql.json" --permissions "role_multiple_create_policy_tester:create" --policy-database "@item.content ne 'Great'"
135+
update Review --config "dab-config.MsSql.json" --permissions "role_multiple_create_policy_tester:update,delete"
136+
update Review_MM --config "dab-config.MsSql.json" --permissions "authenticated:*" --relationship books --relationship.fields "book_id:id" --target.entity Book_MM --cardinality one
137+
update Review_MM --config "dab-config.MsSql.json" --relationship website_users --target.entity WebsiteUser_MM --cardinality one --relationship.fields "websiteuser_id:id"
117138
update BookWebsitePlacement --config "dab-config.MsSql.json" --permissions "authenticated:create,update" --rest true --graphql true
118139
update BookWebsitePlacement --config "dab-config.MsSql.json" --permissions "authenticated:delete" --fields.include "*" --policy-database "@claims.userId eq @item.id"
119140
update Author --config "dab-config.MsSql.json" --permissions "authenticated:create,read,update,delete" --rest true --graphql true
120141
update WebsiteUser --config "dab-config.MsSql.json" --permissions "authenticated:create,read,delete,update" --rest false --graphql "websiteUser:websiteUsers"
142+
update WebsiteUser -c "dab-config.MsSql.json" --relationship reviews --target.entity Review --cardinality many --relationship.fields "id:websiteuser_id"
143+
update WebsiteUser_MM --config "dab-config.MsSql.json" --source website_users_mm --permissions "authenticated:*" --relationship reviews --relationship.fields "id:websiteuser_id" --target.entity Review_MM --cardinality many
121144
update Revenue --config "dab-config.MsSql.json" --permissions "database_policy_tester:create" --policy-database "@item.revenue gt 1000"
122145
update Comic --config "dab-config.MsSql.json" --permissions "authenticated:create,read,update,delete" --rest true --graphql true --relationship myseries --target.entity series --cardinality one
123146
update series --config "dab-config.MsSql.json" --relationship comics --target.entity Comic --cardinality many
@@ -134,6 +157,7 @@ update books_view_with_mapping --config "dab-config.MsSql.json" --map "id:book_i
134157
update BookWebsitePlacement --config "dab-config.MsSql.json" --relationship books --target.entity Book --cardinality one
135158
update SupportedType --config "dab-config.MsSql.json" --map "id:typeid" --permissions "authenticated:create,read,delete,update"
136159
update Author --config "dab-config.MsSql.json" --relationship books --target.entity Book --cardinality many --linking.object book_author_link
160+
update Author_MM --config "dab-config.MsSql.json" --permissions "authenticated:*" --relationship books --relationship.fields "id:id" --target.entity Book_MM --cardinality many --linking.object book_author_link_mm --linking.source.fields "author_id" --linking.target.fields "book_id"
137161
update Notebook --config "dab-config.MsSql.json" --permissions "anonymous:create,update,delete"
138162
update Empty --config "dab-config.MsSql.json" --permissions "anonymous:read"
139163
update Journal --config "dab-config.MsSql.json" --permissions "policy_tester_noupdate:update" --fields.include "*" --policy-database "@item.id ne 1"

src/Config/DataApiBuilderException.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,11 @@ public enum SubStatusCodes
113113
/// <summary>
114114
/// Invalid PK field(s) specified in the request.
115115
/// </summary>
116-
InvalidIdentifierField
116+
InvalidIdentifierField,
117+
/// <summary>
118+
/// Relationship Field's value not found
119+
/// </summary>
120+
RelationshipFieldNotFound
117121
}
118122

119123
public HttpStatusCode StatusCode { get; }

src/Core/Resolvers/BaseSqlQueryBuilder.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,8 +318,16 @@ protected virtual string Build(Predicate? predicate)
318318
/// <summary>
319319
/// Build and join predicates with separator (" AND " by default)
320320
/// </summary>
321-
protected string Build(List<Predicate> predicates, string separator = " AND ")
321+
/// <param name="predicates">List of predicates to be added</param>
322+
/// <param name="separator">Operator to be used with the list of predicates. Default value: AND</param>
323+
/// <param name="isMultipleCreateOperation">Indicates whether the predicates are being formed for a multiple create operation. Default value: false.</param>
324+
protected string Build(List<Predicate> predicates, string separator = " AND ", bool isMultipleCreateOperation = false)
322325
{
326+
if (isMultipleCreateOperation)
327+
{
328+
return "(" + string.Join(separator, predicates.Select(p => Build(p))) + ")";
329+
}
330+
323331
return string.Join(separator, predicates.Select(p => Build(p)));
324332
}
325333

src/Core/Resolvers/IQueryBuilder.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public interface IQueryBuilder
1616
/// query.
1717
/// </summary>
1818
public string Build(SqlQueryStructure structure);
19+
1920
/// <summary>
2021
/// Builds the query specific to the target database for the given
2122
/// SqlInsertStructure object which holds the major components of the

src/Core/Resolvers/IQueryEngine.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,20 @@ public interface IQueryEngine
2222
/// </returns>
2323
public Task<Tuple<JsonDocument?, IMetadata?>> ExecuteAsync(IMiddlewareContext context, IDictionary<string, object?> parameters, string dataSourceName);
2424

25+
/// <summary>
26+
/// Executes the given IMiddlewareContext of the GraphQL query and expects a list of JsonDocument objects back.
27+
/// This method accepts a list of PKs for which to construct and return the response.
28+
/// </summary>
29+
/// <param name="context">IMiddleware context of the GraphQL query</param>
30+
/// <param name="parameters">List of PKs for which the response Json have to be computed and returned.
31+
/// Each Pk is represented by a dictionary where (key, value) as (column name, column value).
32+
/// Primary keys can be of composite and be of any type. Hence, the decision to represent
33+
/// a PK as Dictionary<string, object?>
34+
/// </param>
35+
/// <param name="dataSourceName">DataSource name</param>
36+
/// <returns>Returns the json result and metadata object for the given list of PKs</returns>
37+
public Task<Tuple<JsonDocument?, IMetadata?>> ExecuteMultipleCreateFollowUpQueryAsync(IMiddlewareContext context, List<IDictionary<string, object?>> parameters, string dataSourceName) => throw new NotImplementedException();
38+
2539
/// <summary>
2640
/// Executes the given IMiddlewareContext of the GraphQL and expecting a
2741
/// list of Jsons back.

src/Core/Resolvers/IQueryExecutor.cs

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,28 @@ public interface IQueryExecutor
3434
HttpContext? httpContext = null,
3535
List<string>? args = null);
3636

37+
/// <summary>
38+
/// Executes sql text with the given parameters and
39+
/// uses the function dataReaderHandler to process
40+
/// the results from the DbDataReader and return into an object of type TResult.
41+
/// This method is synchronous. It does not make use of async/await.
42+
/// </summary>
43+
/// <param name="sqltext">SQL text to be executed.</param>
44+
/// <param name="parameters">The parameters used to execute the SQL text.</param>
45+
/// <param name="dataReaderHandler">The function to invoke to handle the results
46+
/// in the DbDataReader obtained after executing the query.</param>
47+
/// <param name="httpContext">Current request httpContext.</param>
48+
/// <param name="args">List of string arguments to the DbDataReader handler.</param>
49+
/// <param name="dataSourceName">dataSourceName against which to run query. Can specify null or empty to run against default db.</param>
50+
/// <returns>An object formed using the results of the query as returned by the given handler.</returns>
51+
public TResult? ExecuteQuery<TResult>(
52+
string sqltext,
53+
IDictionary<string, DbConnectionParam> parameters,
54+
Func<DbDataReader, List<string>?, TResult>? dataReaderHandler,
55+
HttpContext? httpContext = null,
56+
List<string>? args = null,
57+
string dataSourceName = "");
58+
3759
/// <summary>
3860
/// Extracts the rows from the given DbDataReader to populate
3961
/// the JsonArray to be returned.
@@ -63,10 +85,20 @@ public Task<JsonArray> GetJsonArrayAsync(
6385
/// <param name="dbDataReader">A DbDataReader</param>
6486
/// <param name="args">List of columns to extract. Extracts all if unspecified.</param>
6587
/// <returns>Current Result Set in the DbDataReader.</returns>
66-
public Task<DbResultSet> ExtractResultSetFromDbDataReader(
88+
public Task<DbResultSet> ExtractResultSetFromDbDataReaderAsync(
6789
DbDataReader dbDataReader,
6890
List<string>? args = null);
6991

92+
/// <summary>
93+
/// Extracts the current Result Set of DbDataReader and format it
94+
/// so it can be used as a parameter to query execution.
95+
/// This method is synchronous. This does not make use of async await operations.
96+
/// </summary>
97+
/// <param name="dbDataReader">A DbDataReader</param>
98+
/// <param name="args">List of columns to extract. Extracts all if unspecified.</param>
99+
/// <returns>Current Result Set in the DbDataReader.</returns>
100+
public DbResultSet ExtractResultSetFromDbDataReader(DbDataReader dbDataReader, List<string>? args = null);
101+
70102
/// <summary>
71103
/// Extracts the result set corresponding to the operation (update/insert) being executed.
72104
/// For PgSql,MySql, returns the first result set (among the two for update/insert) having non-zero affected rows.
@@ -87,7 +119,18 @@ public Task<DbResultSet> GetMultipleResultSetsIfAnyAsync(
87119
/// <param name="dbDataReader">A DbDataReader.</param>
88120
/// <param name="args">List of string arguments if any.</param>
89121
/// <returns>A dictionary of properties of the DbDataReader like RecordsAffected, HasRows.</returns>
90-
public Task<Dictionary<string, object>> GetResultProperties(
122+
public Task<Dictionary<string, object>> GetResultPropertiesAsync(
123+
DbDataReader dbDataReader,
124+
List<string>? args = null);
125+
126+
/// <summary>
127+
/// Gets the result properties like RecordsAffected, HasRows in a dictionary.
128+
/// This is a synchronous method. It does not make use of async/await.
129+
/// </summary>
130+
/// <param name="dbDataReader">A DbDataReader.</param>
131+
/// <param name="args">List of string arguments if any.</param>
132+
/// <returns>A dictionary of properties of the DbDataReader like RecordsAffected, HasRows.</returns>
133+
public Dictionary<string, object> GetResultProperties(
91134
DbDataReader dbDataReader,
92135
List<string>? args = null);
93136

@@ -98,6 +141,14 @@ public Task<Dictionary<string, object>> GetResultProperties(
98141
/// </summary>
99142
public Task<bool> ReadAsync(DbDataReader reader);
100143

144+
/// <summary>
145+
/// Wrapper for DbDataReader.Read().
146+
/// This will catch certain db errors and throw an exception which can
147+
/// be reported to the user.
148+
/// This method is synchronous. It does not make use of async/await.
149+
/// </summary>
150+
public bool Read(DbDataReader reader);
151+
101152
/// <summary>
102153
/// Modified the properties of the supplied connection to support managed identity access.
103154
/// </summary>

src/Core/Resolvers/MsSqlQueryBuilder.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,24 @@ public string Build(SqlQueryStructure structure)
4242
structure.JoinQueries.Select(
4343
x => $" OUTER APPLY ({Build(x.Value)}) AS {QuoteIdentifier(x.Key)}({dataIdent})"));
4444

45-
string predicates = JoinPredicateStrings(
45+
string predicates;
46+
47+
if (structure.IsMultipleCreateOperation)
48+
{
49+
predicates = JoinPredicateStrings(
50+
structure.GetDbPolicyForOperation(EntityActionOperation.Read),
51+
structure.FilterPredicates,
52+
Build(structure.Predicates, " OR ", isMultipleCreateOperation: true),
53+
Build(structure.PaginationMetadata.PaginationPredicate));
54+
}
55+
else
56+
{
57+
predicates = JoinPredicateStrings(
4658
structure.GetDbPolicyForOperation(EntityActionOperation.Read),
4759
structure.FilterPredicates,
4860
Build(structure.Predicates),
4961
Build(structure.PaginationMetadata.PaginationPredicate));
62+
}
5063

5164
string query = $"SELECT TOP {structure.Limit()} {WrappedColumns(structure)}"
5265
+ $" FROM {fromSql}"

src/Core/Resolvers/MsSqlQueryExecutor.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ public override async Task<DbResultSet> GetMultipleResultSetsIfAnyAsync(
231231
DbDataReader dbDataReader, List<string>? args = null)
232232
{
233233
// From the first result set, we get the count(0/1) of records with given PK.
234-
DbResultSet resultSetWithCountOfRowsWithGivenPk = await ExtractResultSetFromDbDataReader(dbDataReader);
234+
DbResultSet resultSetWithCountOfRowsWithGivenPk = await ExtractResultSetFromDbDataReaderAsync(dbDataReader);
235235
DbResultSetRow? resultSetRowWithCountOfRowsWithGivenPk = resultSetWithCountOfRowsWithGivenPk.Rows.FirstOrDefault();
236236
int numOfRecordsWithGivenPK;
237237

@@ -249,7 +249,7 @@ public override async Task<DbResultSet> GetMultipleResultSetsIfAnyAsync(
249249
}
250250

251251
// The second result set holds the records returned as a result of the executed update/insert operation.
252-
DbResultSet? dbResultSet = await dbDataReader.NextResultAsync() ? await ExtractResultSetFromDbDataReader(dbDataReader) : null;
252+
DbResultSet? dbResultSet = await dbDataReader.NextResultAsync() ? await ExtractResultSetFromDbDataReaderAsync(dbDataReader) : null;
253253

254254
if (dbResultSet is null)
255255
{

0 commit comments

Comments
 (0)