Skip to content

Commit 7b5a709

Browse files
author
Jelte Fennema
committed
Generate sql queries
1 parent f53f901 commit 7b5a709

17 files changed

+366
-56
lines changed

DataGateway.Service/Resolvers/CosmosQueryEngine.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.Text.Json;
77
using System.Threading.Tasks;
8+
using HotChocolate.Resolvers;
89

910
namespace Azure.DataGateway.Services
1011
{
@@ -36,13 +37,14 @@ public void RegisterResolver(GraphQLQueryResolver resolver)
3637
// <summary>
3738
// ExecuteAsync the given named graphql query on the backend.
3839
// </summary>
39-
public async Task<JsonDocument> ExecuteAsync(string graphQLQueryName, IDictionary<string, object> parameters)
40+
public async Task<JsonDocument> ExecuteAsync(IMiddlewareContext context, IDictionary<string, object> parameters)
4041
{
4142
// TODO: fixme we have multiple rounds of serialization/deserialization JsomDocument/JObject
4243
// TODO: add support for nesting
4344
// TODO: add support for join query against another container
4445
// TODO: add support for TOP and Order-by push-down
4546

47+
string graphQLQueryName = context.Selection.Field.Name.Value;
4648
GraphQLQueryResolver resolver = this._metadataStoreProvider.GetQueryResolver(graphQLQueryName);
4749
Container container = this._clientProvider.Client.GetDatabase(resolver.DatabaseName).GetContainer(resolver.ContainerName);
4850
var querySpec = new QueryDefinition(resolver.ParametrizedQuery);
@@ -71,13 +73,14 @@ public async Task<JsonDocument> ExecuteAsync(string graphQLQueryName, IDictionar
7173
return jsonDocument;
7274
}
7375

74-
public async Task<IEnumerable<JsonDocument>> ExecuteListAsync(string graphQLQueryName, IDictionary<string, object> parameters)
76+
public async Task<IEnumerable<JsonDocument>> ExecuteListAsync(IMiddlewareContext context, IDictionary<string, object> parameters)
7577
{
7678
// TODO: fixme we have multiple rounds of serialization/deserialization JsomDocument/JObject
7779
// TODO: add support for nesting
7880
// TODO: add support for join query against another container
7981
// TODO: add support for TOP and Order-by push-down
8082

83+
string graphQLQueryName = context.Selection.Field.Name.Value;
8184
GraphQLQueryResolver resolver = this._metadataStoreProvider.GetQueryResolver(graphQLQueryName);
8285
Container container = this._clientProvider.Client.GetDatabase(resolver.DatabaseName).GetContainer(resolver.ContainerName);
8386
var querySpec = new QueryDefinition(resolver.ParametrizedQuery);

DataGateway.Service/Resolvers/IQueryBuilder.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ public interface IQueryBuilder
1212
// Modifies the inputQuery in such a way that it returns the results as
1313
// a JSON string.
1414
// </summary>
15-
public string Build(string inputQuery, bool isList);
15+
public string QuoteIdentifier(string ident);
16+
public string WrapSubqueryColumn(string column, SqlQueryStructure subquery);
17+
public string Build(SqlQueryStructure structure);
1618
}
1719
}

DataGateway.Service/Resolvers/IQueryEngine.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Generic;
33
using System.Text.Json;
44
using System.Threading.Tasks;
5+
using HotChocolate.Resolvers;
56

67
namespace Azure.DataGateway.Services
78
{
@@ -18,11 +19,11 @@ public interface IQueryEngine
1819
// <summary>
1920
// Executes the given named graphql query on the backend and expecting a single Json back.
2021
// </summary>
21-
public Task<JsonDocument> ExecuteAsync(string graphQLQueryName, IDictionary<string, object> parameters);
22+
public Task<JsonDocument> ExecuteAsync(IMiddlewareContext context, IDictionary<string, object> parameters);
2223

2324
// <summary>
2425
// Executes the given named graphql query on the backend and expecting a list of Jsons back.
2526
// </summary>
26-
public Task<IEnumerable<JsonDocument>> ExecuteListAsync(string graphQLQueryName, IDictionary<string, object> parameters);
27+
public Task<IEnumerable<JsonDocument>> ExecuteListAsync(IMiddlewareContext context, IDictionary<string, object> parameters);
2728
}
2829
}
Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
using System;
2+
using System.Data.Common;
3+
using Microsoft.Data.SqlClient;
4+
using System.Linq;
15

26
namespace Azure.DataGateway.Service.Resolvers
37
{
@@ -6,19 +10,43 @@ namespace Azure.DataGateway.Service.Resolvers
610
/// </summary>
711
public class MsSqlQueryBuilder : IQueryBuilder
812
{
9-
private const string X_FOR_JSON_SUFFIX = " FOR JSON PATH, INCLUDE_NULL_VALUES";
10-
private const string X_WITHOUT_ARRAY_WRAPPER_SUFFIX = "WITHOUT_ARRAY_WRAPPER";
13+
private const string x_ForJsonSuffix = " FOR JSON PATH, INCLUDE_NULL_VALUES";
14+
private const string x_WithoutArrayWrapperSuffix = "WITHOUT_ARRAY_WRAPPER";
1115

12-
public string Build(string inputQuery, bool isList)
16+
private static DbCommandBuilder Builder = new SqlCommandBuilder();
17+
public string QuoteIdentifier(string ident)
1318
{
14-
string queryText = inputQuery + X_FOR_JSON_SUFFIX;
15-
if (!isList)
19+
return Builder.QuoteIdentifier(ident);
20+
}
21+
22+
public string WrapSubqueryColumn(string column, SqlQueryStructure subquery)
23+
{
24+
if (subquery.IsList())
1625
{
17-
queryText += "," + X_WITHOUT_ARRAY_WRAPPER_SUFFIX;
26+
return $"JSON_QUERY (COALESCE({column}, '[]'))";
1827
}
19-
20-
return queryText;
28+
return $"JSON_QUERY ({column})";
2129
}
2230

31+
public string Build(SqlQueryStructure structure)
32+
{
33+
var selectedColumns = String.Join(", ", structure.Columns.Select(x => $"{x.Value} AS {QuoteIdentifier(x.Key)}"));
34+
string fromPart = structure.Table(structure.TableName, structure.TableAlias);
35+
fromPart += String.Join(
36+
"",
37+
structure.JoinQueries.Select(
38+
x => $" OUTER APPLY ({Build(x.Value)}) AS {QuoteIdentifier(x.Key)}({structure.DataIdent})"));
39+
string query = $"SELECT {selectedColumns} FROM {fromPart}";
40+
if (structure.Conditions.Count() > 0)
41+
{
42+
query += $" WHERE {String.Join(" AND ", structure.Conditions)}";
43+
}
44+
query += x_ForJsonSuffix;
45+
if (!structure.IsList())
46+
{
47+
query += "," + x_WithoutArrayWrapperSuffix;
48+
}
49+
return query;
50+
}
2351
}
2452
}
Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
using System;
2+
using System.Linq;
3+
using System.Data.Common;
4+
using Npgsql;
15

26
namespace Azure.DataGateway.Service.Resolvers
37
{
@@ -6,15 +10,40 @@ namespace Azure.DataGateway.Service.Resolvers
610
/// </summary>
711
public class PostgresQueryBuilder : IQueryBuilder
812
{
9-
public string Build(string inputQuery, bool isList)
13+
private static DbCommandBuilder Builder = new NpgsqlCommandBuilder();
14+
public string QuoteIdentifier(string ident)
1015
{
11-
if (!isList)
12-
{
13-
return $"SELECT row_to_json(q) FROM ({inputQuery}) q";
14-
}
16+
return Builder.QuoteIdentifier(ident);
17+
}
1518

16-
return $"SELECT jsonb_agg(row_to_json(q)) FROM ({inputQuery}) q";
19+
public string WrapSubqueryColumn(string column, SqlQueryStructure subquery)
20+
{
21+
return column;
1722
}
1823

24+
public string Build(SqlQueryStructure structure)
25+
{
26+
var selectedColumns = String.Join(", ", structure.Columns.Select(x => $"{x.Value} AS {QuoteIdentifier(x.Key)}"));
27+
string fromPart = structure.Table(structure.TableName, structure.TableAlias);
28+
fromPart += String.Join("", structure.JoinQueries.Select(x => $" LEFT OUTER JOIN LATERAL ({Build(x.Value)}) AS {QuoteIdentifier(x.Key)} ON TRUE"));
29+
string query = $"SELECT {selectedColumns} FROM {fromPart}";
30+
if (structure.Conditions.Count() > 0)
31+
{
32+
query += $" WHERE {String.Join(" AND ", structure.Conditions)}";
33+
}
34+
var subqName = QuoteIdentifier($"subq{structure.Counter.Next()}");
35+
string start;
36+
if (structure.IsList())
37+
{
38+
start = $"SELECT COALESCE(jsonb_agg(to_jsonb({subqName})), '[]') AS {structure.DataIdent} FROM (";
39+
}
40+
else
41+
{
42+
start = $"SELECT to_jsonb({subqName}) AS {structure.DataIdent} FROM (";
43+
}
44+
var end = $") AS {subqName}";
45+
query = $"{start} {query} {end}";
46+
return query;
47+
}
1948
}
2049
}

DataGateway.Service/Resolvers/QueryExecutor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public async Task<DbDataReader> ExecuteQueryAsync(string sqltext, IDictionary<st
4646
}
4747
}
4848

49-
return await cmd.ExecuteReaderAsync(CommandBehavior.SequentialAccess | CommandBehavior.CloseConnection);
49+
return await cmd.ExecuteReaderAsync(CommandBehavior.CloseConnection);
5050
}
5151
}
5252
}

DataGateway.Service/Resolvers/SqlQueryEngine.cs

Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
using System;
44
using System.Collections.Generic;
55
using System.Data.Common;
6+
using System.Text;
67
using System.Text.Json;
78
using System.Threading.Tasks;
9+
using HotChocolate.Resolvers;
810

911
namespace Azure.DataGateway.Service.Resolvers
1012
{
@@ -36,64 +38,73 @@ public void RegisterResolver(GraphQLQueryResolver resolver)
3638
// no-op
3739
}
3840

41+
private static async Task<string> GetJsonStringFromDbReader(DbDataReader dbDataReader)
42+
{
43+
var jsonString = new StringBuilder();
44+
// Even though we only return a single cell, we need this loop for
45+
// MS SQL. Sadly it splits FOR JSON PATH output across multiple
46+
// cells if the JSON consists of more than 2033 bytes:
47+
// Sources:
48+
// 1. https://docs.microsoft.com/en-us/sql/relational-databases/json/format-query-results-as-json-with-for-json-sql-server?view=sql-server-2017#output-of-the-for-json-clause
49+
// 2. https://stackoverflow.com/questions/54973536/for-json-path-results-in-ssms-truncated-to-2033-characters/54973676
50+
// 3. https://docs.microsoft.com/en-us/sql/relational-databases/json/use-for-json-output-in-sql-server-and-in-client-apps-sql-server?view=sql-server-2017#use-for-json-output-in-a-c-client-app
51+
if (await dbDataReader.ReadAsync())
52+
{
53+
jsonString.Append(dbDataReader.GetString(0));
54+
}
55+
return jsonString.ToString();
56+
}
57+
3958
// <summary>
4059
// ExecuteAsync the given named graphql query on the backend.
4160
// </summary>
42-
public async Task<JsonDocument> ExecuteAsync(string graphQLQueryName, IDictionary<string, object> parameters)
61+
public async Task<JsonDocument> ExecuteAsync(IMiddlewareContext context, IDictionary<string, object> parameters)
4362
{
4463
// TODO: add support for nesting
4564
// TODO: add support for join query against another table
4665
// TODO: add support for TOP and Order-by push-down
4766

67+
string graphQLQueryName = context.Selection.Field.Name.Value;
4868
GraphQLQueryResolver resolver = _metadataStoreProvider.GetQueryResolver(graphQLQueryName);
49-
var jsonDocument = JsonDocument.Parse("{ }");
50-
51-
string queryText = _queryBuilder.Build(resolver.ParametrizedQuery, false);
52-
69+
SqlQueryStructure structure = new(context, _metadataStoreProvider, _queryBuilder);
70+
Console.WriteLine(structure.ToString());
5371
// Open connection and execute query using _queryExecutor
5472
//
55-
DbDataReader dbDataReader = await _queryExecutor.ExecuteQueryAsync(queryText, parameters);
73+
DbDataReader dbDataReader = await _queryExecutor.ExecuteQueryAsync(structure.ToString(), parameters);
5674

5775
// Parse Results into Json and return
58-
//
59-
if (await dbDataReader.ReadAsync())
60-
{
61-
jsonDocument = JsonDocument.Parse(dbDataReader.GetString(0));
62-
}
63-
else
76+
if (!dbDataReader.HasRows)
6477
{
65-
Console.WriteLine("Did not return enough rows in the JSON result.");
78+
return null;
6679
}
6780

68-
return jsonDocument;
81+
return JsonDocument.Parse(await GetJsonStringFromDbReader(dbDataReader));
6982
}
7083

7184
// <summary>
7285
// Executes the given named graphql query on the backend and expecting a list of Jsons back.
7386
// </summary>
74-
public async Task<IEnumerable<JsonDocument>> ExecuteListAsync(string graphQLQueryName, IDictionary<string, object> parameters)
87+
public async Task<IEnumerable<JsonDocument>> ExecuteListAsync(IMiddlewareContext context, IDictionary<string, object> parameters)
7588
{
7689
// TODO: add support for nesting
7790
// TODO: add support for join query against another container
7891
// TODO: add support for TOP and Order-by push-down
7992

93+
string graphQLQueryName = context.Selection.Field.Name.Value;
8094
GraphQLQueryResolver resolver = _metadataStoreProvider.GetQueryResolver(graphQLQueryName);
81-
var resultsAsList = new List<JsonDocument>();
82-
string queryText = _queryBuilder.Build(resolver.ParametrizedQuery, true);
83-
DbDataReader dbDataReader = await _queryExecutor.ExecuteQueryAsync(queryText, parameters);
8495

85-
// Deserialize results into list of JsonDocuments and return
96+
SqlQueryStructure structure = new(context, _metadataStoreProvider, _queryBuilder);
97+
Console.WriteLine(structure.ToString());
98+
DbDataReader dbDataReader = await _queryExecutor.ExecuteQueryAsync(structure.ToString(), parameters);
99+
100+
// Parse Results into Json and return
86101
//
87-
if (await dbDataReader.ReadAsync())
88-
{
89-
resultsAsList = JsonSerializer.Deserialize<List<JsonDocument>>(dbDataReader.GetString(0));
90-
}
91-
else
102+
if (!dbDataReader.HasRows)
92103
{
93-
Console.WriteLine("Did not return enough rows in the JSON result.");
104+
return new List<JsonDocument>();
94105
}
95106

96-
return resultsAsList;
107+
return JsonSerializer.Deserialize<List<JsonDocument>>(await GetJsonStringFromDbReader(dbDataReader));
97108
}
98109
}
99110
}

0 commit comments

Comments
 (0)