Skip to content

Commit f557b4d

Browse files
authored
GraphQL stored procedure support for DW (#2008)
## Why make this change? This change adds stored procedure execution support for Datawarehouse. ## What is this change? In this change, the definitions in DwSqlQueryBuilder are created to have the dw queries to get result set information of the stored procedure and to actually execute stored procedure queries. In addition, for both mssql and dwsql the execution logic flows through a single parser. It ensures that both mssql and dw sql use consistent naming to ensure easy parsing of information. ## How was this tested? - [ ] Integration Tests 1.Tested against a warehouse: ![image](https://github.com/Azure/data-api-builder/assets/124841904/fac592c5-d99e-4d12-ac31-7c3ee043c641) 2. Added Tests for stored procedure.
1 parent 302520d commit f557b4d

File tree

11 files changed

+661
-12
lines changed

11 files changed

+661
-12
lines changed

config-generators/dwsql-commands.txt

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ add Notebook --config "dab-config.DwSql.json" --source "notebooks" --permissions
2626
add Journal --config "dab-config.DwSql.json" --source "journals" --rest true --graphql true --permissions "policy_tester_noupdate:create,delete" --source.key-fields "id"
2727
add ArtOfWar --config "dab-config.DwSql.json" --source "aow" --rest true --graphql false --permissions "anonymous:*" --source.key-fields "NoteNum"
2828
add stocks_view_selected --config "dab-config.DwSql.json" --source stocks_view_selected --source.type "view" --source.key-fields "categoryid,pieceid" --permissions "anonymous:*" --rest true --graphql true
29+
add GetBooks --config "dab-config.DwSql.json" --source "get_books" --source.type "stored-procedure" --permissions "anonymous:execute" --rest true --graphql true
30+
add GetPublisher --config "dab-config.DwSql.json" --source "get_publisher_by_id" --source.type "stored-procedure" --permissions "anonymous:execute" --rest true --graphql true --graphql.operation "query"
31+
add GetAuthorsHistoryByFirstName --config "dab-config.DwSql.json" --source "get_authors_history_by_first_name" --source.type "stored-procedure" --source.params "firstName:Aaron" --permissions "anonymous:execute" --rest true --graphql SearchAuthorByFirstName
32+
add CountBooks --config "dab-config.DwSql.json" --source "count_books" --source.type "stored-procedure" --permissions "anonymous:execute" --rest true --graphql true
33+
add InsertBook --config "dab-config.DwSql.json" --source "insert_book" --source.type "stored-procedure" --source.params "title:randomX,publisher_id:1234" --permissions "anonymous:execute" --rest true --graphql true
34+
add DeleteLastInsertedBook --config "dab-config.DwSql.json" --source "delete_last_inserted_book" --source.type "stored-procedure" --permissions "anonymous:execute" --rest true --graphql true
35+
add UpdateBookTitle --config "dab-config.DwSql.json" --source "update_book_title" --source.type "stored-procedure" --source.params "id:1,title:Testing Tonight" --permissions "anonymous:execute" --rest true --graphql true
36+
add InsertAndDisplayAllBooksUnderGivenPublisher --config "dab-config.DwSql.json" --source "insert_and_display_all_books_for_given_publisher" --source.type "stored-procedure" --source.params "title:MyTitle,publisher_name:MyPublisher" --permissions "anonymous:execute" --rest true --graphql true
2937
update stocks_price --config "dab-config.DwSql.json" --permissions "anonymous:read"
3038
update stocks_price --config "dab-config.DwSql.json" --permissions "TestNestedFilterFieldIsNull_ColumnForbidden:read" --fields.exclude "price"
3139
update stocks_price --config "dab-config.DwSql.json" --permissions "TestNestedFilterFieldIsNull_EntityReadForbidden:create"
@@ -136,4 +144,13 @@ update Journal --config "dab-config.DwSql.json" --permissions "policy_tester_upd
136144
update Journal --config "dab-config.DwSql.json" --permissions "policy_tester_update_noread:delete" --fields.include "*" --policy-database "@item.id eq 1"
137145
update Journal --config "dab-config.DwSql.json" --permissions "authorizationHandlerTester:read"
138146
update ArtOfWar --config "dab-config.DwSql.json" --permissions "authenticated:*" --map "DetailAssessmentAndPlanning:始計,WagingWar:作戰,StrategicAttack:謀攻,NoteNum:┬─┬ノ( º _ ºノ)"
139-
update stocks_view_selected --config "dab-config.DwSql.json" --permissions "authenticated:create,read,update,delete"
147+
update stocks_view_selected --config "dab-config.DwSql.json" --permissions "authenticated:create,read,update,delete"
148+
update InsertBook --config "dab-config.DwSql.json" --permissions "authenticated:execute"
149+
update DeleteLastInsertedBook --config "dab-config.DwSql.json" --permissions "authenticated:execute"
150+
update UpdateBookTitle --config "dab-config.DwSql.json" --permissions "authenticated:execute"
151+
update InsertAndDisplayAllBooksUnderGivenPublisher --config "dab-config.DwSql.json" --permissions "authenticated:execute"
152+
update GetPublisher --config "dab-config.DwSql.json" --permissions "authenticated:execute"
153+
update GetBooks --config "dab-config.DwSql.json" --permissions "authenticated:execute" --graphql.operation "Query"
154+
update CountBooks --config "dab-config.DwSql.json" --permissions "authenticated:execute"
155+
update Sales --config "dab-config.DwSql.json" --permissions "authenticated:*"
156+
update GetAuthorsHistoryByFirstName --config "dab-config.DwSql.json" --permissions "authenticated:execute"

src/Core/Resolvers/BaseSqlQueryBuilder.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ public abstract class BaseSqlQueryBuilder
2020
{
2121
public const string SCHEMA_NAME_PARAM = "schemaName";
2222
public const string TABLE_NAME_PARAM = "tableName";
23+
public const string STOREDPROC_COLUMN_NAME = "name";
24+
public const string STOREDPROC_COLUMN_SYSTEMTYPENAME = "system_type_name";
25+
public const string STOREDPROC_COLUMN_ISNULLABLE = "is_nullable";
2326

2427
/// <summary>
2528
/// Predicate added to the query when no other predicates exist.

src/Core/Resolvers/DWSqlQueryBuilder.cs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,8 @@ public string Build(SqlDeleteStructure structure)
209209
/// <inheritdoc />
210210
public string Build(SqlExecuteStructure structure)
211211
{
212-
throw new NotImplementedException("DataWarehouse sql currently does not support executes");
212+
return $"EXECUTE {QuoteIdentifier(structure.DatabaseObject.SchemaName)}.{QuoteIdentifier(structure.DatabaseObject.Name)} " +
213+
$"{BuildProcedureParameterList(structure.ProcedureParameters)}";
213214
}
214215

215216
/// <inheritdoc />
@@ -306,7 +307,8 @@ private string WrappedColumns(SqlQueryStructure structure)
306307
/// <inheritdoc />
307308
public string BuildStoredProcedureResultDetailsQuery(string databaseObjectName)
308309
{
309-
throw new NotImplementedException("DataWarehouse sql currently does not support stored procedures");
310+
string query = $"EXEC sp_describe_first_result_set @tsql = N'{databaseObjectName}';";
311+
return query;
310312
}
311313

312314
/// <summary>
@@ -338,5 +340,23 @@ public string BuildFetchEnabledTriggersQuery()
338340

339341
return query;
340342
}
343+
344+
/// <summary>
345+
/// Builds the parameter list for the stored procedure execute call
346+
/// paramKeys are the user-generated procedure parameter names
347+
/// paramValues are the auto-generated, parameterized values (@param0, @param1..)
348+
/// </summary>
349+
private static string BuildProcedureParameterList(Dictionary<string, object> procedureParameters)
350+
{
351+
StringBuilder sb = new();
352+
foreach ((string paramKey, object paramValue) in procedureParameters)
353+
{
354+
sb.Append($"@{paramKey} = {paramValue}, ");
355+
}
356+
357+
string parameterList = sb.ToString();
358+
// If at least one parameter added, remove trailing comma and space, else return empty string
359+
return parameterList.Length > 0 ? parameterList[..^2] : parameterList;
360+
}
341361
}
342362
}

src/Core/Resolvers/MsSqlQueryBuilder.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -472,8 +472,10 @@ private static string BuildProcedureParameterList(Dictionary<string, object> pro
472472
/// </summary>
473473
public string BuildStoredProcedureResultDetailsQuery(string databaseObjectName)
474474
{
475+
// The system type name column is aliased while the other columns are not to ensure
476+
// names are consistent across different sql implementations as all go through same deserialization logic
475477
string query = "SELECT " +
476-
"name as result_field_name, TYPE_NAME(system_type_id) as result_type, is_nullable " +
478+
$"{STOREDPROC_COLUMN_NAME}, TYPE_NAME(system_type_id) as {STOREDPROC_COLUMN_SYSTEMTYPENAME}, {STOREDPROC_COLUMN_ISNULLABLE} " +
477479
"FROM " +
478480
"sys.dm_exec_describe_first_result_set_for_object (" +
479481
$"OBJECT_ID('{databaseObjectName}'), 0) " +

src/Core/Services/MetadataProviders/SqlMetadataProvider.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -919,7 +919,7 @@ await FillSchemaForStoredProcedureAsync(
919919
GetDatabaseObjectName(entityName),
920920
GetStoredProcedureDefinition(entityName));
921921

922-
if (GetDatabaseType() == DatabaseType.MSSQL)
922+
if (GetDatabaseType() == DatabaseType.MSSQL || GetDatabaseType() == DatabaseType.DWSQL)
923923
{
924924
await PopulateResultSetDefinitionsForStoredProcedureAsync(
925925
GetSchemaName(entityName),
@@ -985,9 +985,9 @@ private async Task PopulateResultSetDefinitionsForStoredProcedureAsync(
985985
// one row in the result set.
986986
foreach (JsonElement element in sqlResult.RootElement.EnumerateArray())
987987
{
988-
string resultFieldName = element.GetProperty("result_field_name").ToString();
989-
Type resultFieldType = SqlToCLRType(element.GetProperty("result_type").ToString());
990-
bool isResultFieldNullable = element.GetProperty("is_nullable").GetBoolean();
988+
string resultFieldName = element.GetProperty(BaseSqlQueryBuilder.STOREDPROC_COLUMN_NAME).ToString();
989+
Type resultFieldType = SqlToCLRType(element.GetProperty(BaseSqlQueryBuilder.STOREDPROC_COLUMN_SYSTEMTYPENAME).ToString());
990+
bool isResultFieldNullable = element.GetProperty(BaseSqlQueryBuilder.STOREDPROC_COLUMN_ISNULLABLE).GetBoolean();
991991

992992
// Store the dictionary containing result set field with its type as Columns
993993
storedProcedureDefinition.Columns.TryAdd(resultFieldName, new(resultFieldType) { IsNullable = isResultFieldNullable });

src/Core/Services/TypeHelper.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,11 @@ public static JsonDataType GetJsonDataTypeFromSystemType(Type type)
227227
/// <exception>Failed type conversion.</exception>"
228228
public static Type GetSystemTypeFromSqlDbType(string sqlDbTypeName)
229229
{
230-
if (Enum.TryParse(sqlDbTypeName, ignoreCase: true, out SqlDbType sqlDbType))
230+
// Remove the length specifier from the type name if it exists.Example: varchar(50) -> varchar
231+
int separatorIndex = sqlDbTypeName.IndexOf('(');
232+
string baseType = separatorIndex == -1 ? sqlDbTypeName : sqlDbTypeName.Substring(0, separatorIndex);
233+
234+
if (Enum.TryParse(baseType, ignoreCase: true, out SqlDbType sqlDbType))
231235
{
232236
if (_sqlDbTypeToType.TryGetValue(sqlDbType, out Type? value))
233237
{

src/Service.Tests/DatabaseSchema-DwSql.sql

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,16 @@ DROP TABLE IF EXISTS GQLmappings;
3030
DROP TABLE IF EXISTS bookmarks;
3131
DROP TABLE IF EXISTS mappedbookmarks;
3232
DROP TABLE IF EXISTS publishers;
33+
DROP TABLE IF EXISTS authors_history;
34+
DROP PROCEDURE IF EXISTS get_books;
35+
DROP PROCEDURE IF EXISTS get_book_by_id;
36+
DROP PROCEDURE IF EXISTS get_publisher_by_id;
37+
DROP PROCEDURE IF EXISTS count_books;
38+
DROP PROCEDURE IF EXISTS get_authors_history_by_first_name;
39+
DROP PROCEDURE IF EXISTS insert_book;
40+
DROP PROCEDURE IF EXISTS delete_last_inserted_book;
41+
DROP PROCEDURE IF EXISTS update_book_title;
42+
DROP PROCEDURE IF EXISTS insert_and_display_all_books_for_given_publisher;
3343
DROP SCHEMA IF EXISTS [foo];
3444
COMMIT;
3545

@@ -196,8 +206,75 @@ CREATE TABLE type_table(
196206
uuid_types uniqueidentifier
197207
);
198208

209+
CREATE TABLE authors_history (
210+
id int NOT NULL,
211+
first_name varchar(100) NOT NULL,
212+
middle_name varchar(100),
213+
last_name varchar(100) NOT NULL,
214+
year_of_publish int,
215+
books_published int
216+
);
217+
218+
EXEC('CREATE PROCEDURE get_publisher_by_id @id int AS
219+
SELECT * FROM dbo.publishers
220+
WHERE id = @id');
221+
EXEC('CREATE PROCEDURE get_books AS
222+
SELECT * FROM dbo.books');
223+
EXEC('CREATE PROCEDURE get_book_by_id @id int AS
224+
SELECT * FROM dbo.books
225+
WHERE id = @id');
226+
EXEC('CREATE PROCEDURE count_books AS
227+
SELECT COUNT(*) AS total_books FROM dbo.books');
228+
EXEC('CREATE PROCEDURE get_authors_history_by_first_name @firstName varchar(100) AS
229+
BEGIN
230+
SELECT
231+
concat(first_name, '' '', (middle_name + '' ''), last_name) as author_name,
232+
min(year_of_publish) as first_publish_year,
233+
sum(books_published) as total_books_published
234+
FROM
235+
authors_history
236+
WHERE
237+
first_name=@firstName
238+
GROUP BY
239+
concat(first_name, '' '', (middle_name + '' ''), last_name)
240+
END');
241+
EXEC('CREATE PROCEDURE insert_book @book_id int, @title varchar(max), @publisher_id int AS
242+
INSERT INTO dbo.books(id, title, publisher_id) VALUES (@book_id, @title, @publisher_id)');
243+
EXEC('CREATE PROCEDURE delete_last_inserted_book AS
244+
BEGIN
245+
DELETE FROM dbo.books
246+
WHERE
247+
id = (select max(id) from dbo.books)
248+
END');
249+
EXEC('CREATE PROCEDURE update_book_title @id int, @title varchar(max) AS
250+
BEGIN
251+
UPDATE dbo.books SET title = @title WHERE id = @id
252+
SELECT * from dbo.books WHERE id = @id
253+
END');
254+
EXEC('CREATE PROCEDURE insert_and_display_all_books_for_given_publisher @book_id int,@title varchar(max), @publisher_name varchar(max) AS
255+
BEGIN
256+
DECLARE @publisher_id AS INT;
257+
SET @publisher_id = (SELECT id FROM dbo.publishers WHERE name = @publisher_name);
258+
INSERT INTO dbo.books(id, title, publisher_id)
259+
VALUES(@book_id, @title, @publisher_id);
260+
261+
SELECT * FROM dbo.books WHERE publisher_id = @publisher_id;
262+
END');
199263
INSERT INTO authors(id, name, birthdate) VALUES (123, 'Jelte', '2001-01-01'), (124, 'Aniruddh', '2002-02-02'), (125, 'Aniruddh', '2001-01-01'), (126, 'Aaron', '2001-01-01');
200264

265+
INSERT INTO authors_history(id, first_name, middle_name, last_name, year_of_publish, books_published)
266+
VALUES
267+
(1, 'Isaac', null, 'Asimov', 1993, 6),
268+
(2, 'Robert', 'A.', 'Heinlein', 1886, null),
269+
(3, 'Robert', null, 'Silvenberg', null, null),
270+
(4, 'Dan', null, 'Simmons', 1759, 3),
271+
(5, 'Isaac', null, 'Asimov', 2000, null),
272+
(6, 'Robert', 'A.', 'Heinlein', 1899, 2),
273+
(7, 'Isaac', null, 'Silvenberg', 1664, null),
274+
(8, 'Dan', null, 'Simmons', 1799, 3),
275+
(9, 'Aaron', null, 'Mitchells', 2001, 1),
276+
(10, 'Aaron', 'F.', 'Burtle', null, null)
277+
201278
INSERT INTO GQLmappings(__column1, __column2, column3) VALUES (1, 'Incompatible GraphQL Name', 'Compatible GraphQL Name');
202279
INSERT INTO GQLmappings(__column1, __column2, column3) VALUES (3, 'Old Value', 'Record to be Updated');
203280
INSERT INTO GQLmappings(__column1, __column2, column3) VALUES (4, 'Lost Record', 'Record to be Deleted');

0 commit comments

Comments
 (0)