From 7b20d6e029394041f175c80d5de7ef086c7287e7 Mon Sep 17 00:00:00 2001 From: Yuriy Ivon Date: Sun, 30 Jul 2023 09:16:25 +0300 Subject: [PATCH] Introduced inline parameters for raw queries to provide more flexibility around query generation --- .../Common/InlineParameterFormatter.cs | 20 ++++ .../ElasticsearchRawQueryBuilder.cs | 25 ++--- .../ElasticsearchRawQueryExecutorFactory.cs | 1 + .../MongoDb/MongoDbRawQueryBuilder.cs | 22 ++--- .../Databases/Sql/SqlRawQueryBuilder.cs | 10 +- .../Model/RawQueryParameter.cs | 4 + .../ElasticsearchRawQueryBuilderTests.cs | 45 +++++++++ .../Databases/MongoDbRawQueryBuilderTests.cs | 35 +++++++ .../Databases/SqlRawQueryBuilderTests.cs | 21 +++- .../Utils/SampleInputs.cs | 99 +++++++++++++++++-- .../Utils/StreamExtensions.cs | 14 +++ 11 files changed, 255 insertions(+), 41 deletions(-) create mode 100644 src/DatabaseBenchmark/Databases/Common/InlineParameterFormatter.cs create mode 100644 tests/DatabaseBenchmark.Tests/Databases/ElasticsearchRawQueryBuilderTests.cs create mode 100644 tests/DatabaseBenchmark.Tests/Databases/MongoDbRawQueryBuilderTests.cs create mode 100644 tests/DatabaseBenchmark.Tests/Utils/StreamExtensions.cs diff --git a/src/DatabaseBenchmark/Databases/Common/InlineParameterFormatter.cs b/src/DatabaseBenchmark/Databases/Common/InlineParameterFormatter.cs new file mode 100644 index 0000000..8009ba2 --- /dev/null +++ b/src/DatabaseBenchmark/Databases/Common/InlineParameterFormatter.cs @@ -0,0 +1,20 @@ +using DatabaseBenchmark.Common; + +namespace DatabaseBenchmark.Databases.Common +{ + public static class InlineParameterFormatter + { + public static string Format(string format, object value) => + value switch + { + null => null, + bool b => b.ToString().ToLower(), + int n => n.ToString(format), + long n => n.ToString(format), + double n => n.ToString(format), + DateTime dt => dt.ToString(format ?? "s"), + Guid g => g.ToString(format), + _ => value.ToString() + }; + } +} diff --git a/src/DatabaseBenchmark/Databases/Elasticsearch/ElasticsearchRawQueryBuilder.cs b/src/DatabaseBenchmark/Databases/Elasticsearch/ElasticsearchRawQueryBuilder.cs index 8b6085a..30d2358 100644 --- a/src/DatabaseBenchmark/Databases/Elasticsearch/ElasticsearchRawQueryBuilder.cs +++ b/src/DatabaseBenchmark/Databases/Elasticsearch/ElasticsearchRawQueryBuilder.cs @@ -1,9 +1,12 @@ using DatabaseBenchmark.Core.Interfaces; +using DatabaseBenchmark.Databases.Common; using DatabaseBenchmark.Databases.Elasticsearch.Interfaces; +using DatabaseBenchmark.Model; using Elasticsearch.Net; using Nest; using System.Reflection; using System.Text; +using System.Text.Json; using RawQuery = DatabaseBenchmark.Model.RawQuery; namespace DatabaseBenchmark.Databases.Elasticsearch @@ -58,12 +61,12 @@ private string ApplyParameters(string queryText) if (rawValue is IEnumerable rawCollection) { - var aliases = rawCollection.Select(v => FormatValue(v)).ToArray(); + var aliases = rawCollection.Select(v => FormatParameter(parameter, v)).ToArray(); parameterString = string.Join(", ", aliases); } else { - parameterString = FormatValue(rawValue); + parameterString = FormatParameter(parameter, rawValue); } queryText = queryText.Replace($"${{{parameter.Name}}}", parameterString); @@ -72,19 +75,6 @@ private string ApplyParameters(string queryText) return queryText; } - private static string FormatValue(object value) - { - if (value != null) - { - var stringValue = value.ToString(); - - return (value is bool || value is int || value is long || value is double) - ? stringValue : $"\"{stringValue.Replace("\"", "\\\"")}\""; - } - - return "null"; - } - private static void CopyPublicProperties(T source, T destination) { foreach(var property in typeof(T).GetProperties( @@ -97,5 +87,10 @@ private static void CopyPublicProperties(T source, T destination) } } } + + private static string FormatParameter(RawQueryParameter parameter, object value) => + parameter.Inline + ? InlineParameterFormatter.Format(parameter.InlineFormat, value) + : JsonSerializer.Serialize(value); } } diff --git a/src/DatabaseBenchmark/Databases/Elasticsearch/ElasticsearchRawQueryExecutorFactory.cs b/src/DatabaseBenchmark/Databases/Elasticsearch/ElasticsearchRawQueryExecutorFactory.cs index 601006c..3e5b4fd 100644 --- a/src/DatabaseBenchmark/Databases/Elasticsearch/ElasticsearchRawQueryExecutorFactory.cs +++ b/src/DatabaseBenchmark/Databases/Elasticsearch/ElasticsearchRawQueryExecutorFactory.cs @@ -22,6 +22,7 @@ public ElasticsearchRawQueryExecutorFactory( Container.RegisterDecorator(Lifestyle); Container.Register(createClient, Lifestyle); + //TODO: Find a better way to instantiate the default serializer Container.Register(() => Container.GetInstance().RequestResponseSerializer, Lifestyle); Container.Register(Lifestyle); Container.Register(Lifestyle); diff --git a/src/DatabaseBenchmark/Databases/MongoDb/MongoDbRawQueryBuilder.cs b/src/DatabaseBenchmark/Databases/MongoDb/MongoDbRawQueryBuilder.cs index ee59ae1..6b0015b 100644 --- a/src/DatabaseBenchmark/Databases/MongoDb/MongoDbRawQueryBuilder.cs +++ b/src/DatabaseBenchmark/Databases/MongoDb/MongoDbRawQueryBuilder.cs @@ -1,7 +1,9 @@ using DatabaseBenchmark.Core.Interfaces; +using DatabaseBenchmark.Databases.Common; using DatabaseBenchmark.Databases.MongoDb.Interfaces; using DatabaseBenchmark.Model; using MongoDB.Bson; +using System.Text.Json; namespace DatabaseBenchmark.Databases.MongoDb { @@ -47,12 +49,12 @@ private string ApplyParameters(string queryText) if (rawValue is IEnumerable rawCollection) { - var aliases = rawCollection.Select(v => FormatValue(v)).ToArray(); + var aliases = rawCollection.Select(v => FormatParameter(parameter, v)).ToArray(); parameterString = string.Join(", ", aliases); } else { - parameterString = FormatValue(rawValue); + parameterString = FormatParameter(parameter, rawValue); } queryText = queryText.Replace($"${{{parameter.Name}}}", parameterString); @@ -61,17 +63,9 @@ private string ApplyParameters(string queryText) return queryText; } - private static string FormatValue(object value) - { - if (value != null) - { - var stringValue = value.ToString(); - - return (value is bool || value is int || value is long || value is double) - ? stringValue : $"\"{stringValue.Replace("\"", "\\\"")}\""; - } - - return "null"; - } + private static string FormatParameter(RawQueryParameter parameter, object value) => + parameter.Inline + ? InlineParameterFormatter.Format(parameter.InlineFormat, value) + : JsonSerializer.Serialize(value); } } diff --git a/src/DatabaseBenchmark/Databases/Sql/SqlRawQueryBuilder.cs b/src/DatabaseBenchmark/Databases/Sql/SqlRawQueryBuilder.cs index b9bec9f..37bac01 100644 --- a/src/DatabaseBenchmark/Databases/Sql/SqlRawQueryBuilder.cs +++ b/src/DatabaseBenchmark/Databases/Sql/SqlRawQueryBuilder.cs @@ -1,4 +1,5 @@ using DatabaseBenchmark.Core.Interfaces; +using DatabaseBenchmark.Databases.Common; using DatabaseBenchmark.Databases.Sql.Interfaces; using DatabaseBenchmark.Model; @@ -48,12 +49,12 @@ private string ApplyParameters(string queryText) if (rawValue is IEnumerable rawCollection) { - var aliases = rawCollection.Select(v => _parametersBuilder.Append(v, parameter.Type)).ToArray(); + var aliases = rawCollection.Select(v => BuildParameter(parameter, v)).ToArray(); parameterString = string.Join(", ", aliases); } else { - parameterString = _parametersBuilder.Append(rawValue, parameter.Type); + parameterString = BuildParameter(parameter, rawValue); } queryText = queryText.Replace($"${{{parameter.Name}}}", parameterString); @@ -61,5 +62,10 @@ private string ApplyParameters(string queryText) return queryText; } + + private string BuildParameter(RawQueryParameter parameter, object value) => + parameter.Inline + ? InlineParameterFormatter.Format(parameter.InlineFormat, value) + : _parametersBuilder.Append(value, parameter.Type); } } diff --git a/src/DatabaseBenchmark/Model/RawQueryParameter.cs b/src/DatabaseBenchmark/Model/RawQueryParameter.cs index 2357eeb..b7e46e9 100644 --- a/src/DatabaseBenchmark/Model/RawQueryParameter.cs +++ b/src/DatabaseBenchmark/Model/RawQueryParameter.cs @@ -16,5 +16,9 @@ public class RawQueryParameter public bool RandomizeValue { get; set; } = false; public ValueRandomizationRule ValueRandomizationRule { get; set; } = new ValueRandomizationRule(); + + public bool Inline { get; set; } = false; + + public string InlineFormat { get; set; } } } diff --git a/tests/DatabaseBenchmark.Tests/Databases/ElasticsearchRawQueryBuilderTests.cs b/tests/DatabaseBenchmark.Tests/Databases/ElasticsearchRawQueryBuilderTests.cs new file mode 100644 index 0000000..28a73be --- /dev/null +++ b/tests/DatabaseBenchmark.Tests/Databases/ElasticsearchRawQueryBuilderTests.cs @@ -0,0 +1,45 @@ +using DatabaseBenchmark.Databases.Elasticsearch; +using DatabaseBenchmark.Tests.Utils; +using Nest; +using System.IO; +using Xunit; + +namespace DatabaseBenchmark.Tests.Databases +{ + public class ElasticsearchRawQueryBuilderTests + { + [Fact] + public void BuildParameterizedQuery() + { + var query = SampleInputs.RawElasticsearchQuery; + //TODO: Find a better way to instantiate the default serializer + var serializer = new ElasticClient().RequestResponseSerializer; + var builder = new ElasticsearchRawQueryBuilder(query, serializer, null); + + var searchRequest = builder.Build(); + + using var stream = new MemoryStream(); + serializer.Serialize(searchRequest, stream); + var queryText = stream.ReadAsString(); + + Assert.Equal(@"{""query"":{""bool"":{""must"":[{""term"":{""category"":{""value"":""ABC""}}},{""range"":{""createdDate"":{""gte"":""2020-01-02T03:04:05""}}},{""range"":{""price"":{""lte"":25.5}}},{""term"":{""available"":{""value"":true}}}]}}}", queryText); + } + + [Fact] + public void BuildInlineParameterizedQuery() + { + var query = SampleInputs.RawElasticsearchInlineQuery; + //TODO: Find a better way to instantiate the default serializer + var serializer = new ElasticClient().RequestResponseSerializer; + var builder = new ElasticsearchRawQueryBuilder(query, serializer, null); + + var searchRequest = builder.Build(); + + using var stream = new MemoryStream(); + serializer.Serialize(searchRequest, stream); + var queryText = stream.ReadAsString(); + + Assert.Equal(@"{""query"":{""bool"":{""must"":[{""term"":{""category"":{""value"":""ABC""}}},{""range"":{""createdDate"":{""gte"":""2020-01-02T03:04:05""}}},{""range"":{""price"":{""lte"":25.5}}},{""term"":{""available"":{""value"":true}}}]}}}", queryText); + } + } +} diff --git a/tests/DatabaseBenchmark.Tests/Databases/MongoDbRawQueryBuilderTests.cs b/tests/DatabaseBenchmark.Tests/Databases/MongoDbRawQueryBuilderTests.cs new file mode 100644 index 0000000..df3bc21 --- /dev/null +++ b/tests/DatabaseBenchmark.Tests/Databases/MongoDbRawQueryBuilderTests.cs @@ -0,0 +1,35 @@ +using DatabaseBenchmark.Databases.MongoDb; +using DatabaseBenchmark.Tests.Utils; +using Microsoft.IdentityModel.Tokens; +using MongoDB.Bson; +using Xunit; + +namespace DatabaseBenchmark.Tests.Databases +{ + public class MongoDbRawQueryBuilderTests + { + [Fact] + public void BuildParameterizedQuery() + { + var query = SampleInputs.RawMongoDbQuery; + var builder = new MongoDbRawQueryBuilder(query, null); + + var queryParts = builder.Build(); + + var queryText = new BsonArray(queryParts).ToString(); + Assert.Equal(@"[{ ""$match"" : { ""$and"" : [{ ""category"" : ""ABC"" }, { ""createdDate"" : { ""$gte"" : ""2020-01-02T03:04:05"" } }, { ""price"" : { ""$lte"" : 25.5 } }, { ""available"" : true }] } }]", queryText); + } + + [Fact] + public void BuildInlineParameterizedQuery() + { + var query = SampleInputs.RawMongoDbInlineQuery; + var builder = new MongoDbRawQueryBuilder(query, null); + + var queryParts = builder.Build(); + + var queryText = new BsonArray(queryParts).ToString(); + Assert.Equal(@"[{ ""$match"" : { ""$and"" : [{ ""category"" : ""ABC"" }, { ""createdDate"" : { ""$gte"" : ""2020-01-02T03:04:05"" } }, { ""price"" : { ""$lte"" : 25.5 } }, { ""available"" : true }] } }]", queryText); + } + } +} diff --git a/tests/DatabaseBenchmark.Tests/Databases/SqlRawQueryBuilderTests.cs b/tests/DatabaseBenchmark.Tests/Databases/SqlRawQueryBuilderTests.cs index 0d2c006..7f431c7 100644 --- a/tests/DatabaseBenchmark.Tests/Databases/SqlRawQueryBuilderTests.cs +++ b/tests/DatabaseBenchmark.Tests/Databases/SqlRawQueryBuilderTests.cs @@ -1,6 +1,7 @@ using DatabaseBenchmark.Databases.Sql; using DatabaseBenchmark.Model; using DatabaseBenchmark.Tests.Utils; +using System; using Xunit; namespace DatabaseBenchmark.Tests.Databases @@ -16,14 +17,30 @@ public void BuildParameterizedQuery() var queryText = builder.Build(); - Assert.Equal("SELECT * FROM Sample WHERE Category = @p0", queryText); + Assert.Equal("SELECT * FROM Sample WHERE Category = @p0 AND CreatedDate >= @p1 AND Price <= @p2 AND Available = @p3", queryText); var reference = new SqlQueryParameter[] { - new ('@', "p0", "ABC", ColumnType.String) + new ('@', "p0", "ABC", ColumnType.String), + new ('@', "p1", new DateTime(2020, 1, 2, 3, 4, 5), ColumnType.DateTime), + new ('@', "p2", 25.5, ColumnType.Double), + new ('@', "p3", true, ColumnType.Boolean) }; Assert.Equal(reference, parametersBuilder.Parameters); } + + [Fact] + public void BuildInlineParameterizedQuery() + { + var query = SampleInputs.RawSqlInlineQuery; + var parametersBuilder = new SqlParametersBuilder(); + var builder = new SqlRawQueryBuilder(query, parametersBuilder, null); + + var queryText = builder.Build(); + + Assert.Equal("SELECT * FROM Sample WHERE Category = 'ABC' AND CreatedDate >= '2020-01-02T03:04:05' AND Price <= 25.5", queryText); + Assert.Empty(parametersBuilder.Parameters); + } } } diff --git a/tests/DatabaseBenchmark.Tests/Utils/SampleInputs.cs b/tests/DatabaseBenchmark.Tests/Utils/SampleInputs.cs index 89e0ef0..6358a3a 100644 --- a/tests/DatabaseBenchmark.Tests/Utils/SampleInputs.cs +++ b/tests/DatabaseBenchmark.Tests/Utils/SampleInputs.cs @@ -1,4 +1,5 @@ using DatabaseBenchmark.Model; +using System; using System.Linq; namespace DatabaseBenchmark.Tests.Utils @@ -199,15 +200,97 @@ public static Query AllArgumentsQueryRandomizeInclusionPartial public static RawQuery RawSqlQuery => new() { - Text = "SELECT * FROM Sample WHERE Category = ${category}", - Parameters = new RawQueryParameter[] + Text = "SELECT * FROM Sample WHERE Category = ${category} AND CreatedDate >= ${minDate} AND Price <= ${maxPrice} AND Available = ${available}", + Parameters = RawQueryParameters + }; + + public static RawQuery RawSqlInlineQuery => new() + { + Text = "SELECT * FROM Sample WHERE Category = '${category}' AND CreatedDate >= '${minDate}' AND Price <= ${maxPrice}", + Parameters = RawQueryInlineParameters + }; + + public static RawQuery RawElasticsearchQuery => new() + { + Text = @"{""query"":{""bool"":{""must"":[{""term"":{""category"":{""value"":${category}}}},{""range"":{""createdDate"":{""gte"":${minDate}}}},{""range"":{""price"":{""lte"":${maxPrice}}}},{""term"":{""available"":{""value"":${available}}}}]}}}", + Parameters = RawQueryParameters + }; + + public static RawQuery RawElasticsearchInlineQuery => new() + { + Text = @"{""query"":{""bool"":{""must"":[{""term"":{""category"":{""value"":""${category}""}}},{""range"":{""createdDate"":{""gte"":""${minDate}""}}},{""range"":{""price"":{""lte"":${maxPrice}}}},{""term"":{""available"":{""value"":${available}}}}]}}}", + Parameters = RawQueryInlineParameters + }; + + public static RawQuery RawMongoDbQuery => new() + { + Text = @"[{ ""$match"" : { ""$and"" : [{ ""category"" : ${category} }, { ""createdDate"" : { ""$gte"" : ${minDate} } }, { ""price"" : { ""$lte"" : ${maxPrice} } }, { ""available"" : ${available} }] } }]", + Parameters = RawQueryParameters + }; + + public static RawQuery RawMongoDbInlineQuery => new() + { + Text = @"[{ ""$match"" : { ""$and"" : [{ ""category"" : ""${category}"" }, { ""createdDate"" : { ""$gte"" : ""${minDate}"" } }, { ""price"" : { ""$lte"" : ${maxPrice} } }, { ""available"" : ${available} }] } }]", + Parameters = RawQueryInlineParameters + }; + + public static RawQueryParameter[] RawQueryParameters => new[] + { + new RawQueryParameter { - new RawQueryParameter - { - Name = "category", - Type = ColumnType.String, - Value = "ABC" - } + Name = "category", + Type = ColumnType.String, + Value = "ABC" + }, + new RawQueryParameter + { + Name = "minDate", + Type = ColumnType.DateTime, + Value = new DateTime(2020, 1, 2, 3, 4, 5) + }, + new RawQueryParameter + { + Name = "maxPrice", + Type = ColumnType.Double, + Value = 25.5 + }, + new RawQueryParameter + { + Name = "available", + Type = ColumnType.Boolean, + Value = true + } + }; + + public static RawQueryParameter[] RawQueryInlineParameters => new[] + { + new RawQueryParameter + { + Name = "category", + Type = ColumnType.String, + Value = "ABC", + Inline = true + }, + new RawQueryParameter + { + Name = "minDate", + Type = ColumnType.DateTime, + Value = new DateTime(2020, 1, 2, 3, 4, 5), + Inline = true + }, + new RawQueryParameter + { + Name = "maxPrice", + Type = ColumnType.Double, + Value = 25.5, + Inline = true + }, + new RawQueryParameter + { + Name = "available", + Type = ColumnType.Boolean, + Value = true, + Inline = true } }; } diff --git a/tests/DatabaseBenchmark.Tests/Utils/StreamExtensions.cs b/tests/DatabaseBenchmark.Tests/Utils/StreamExtensions.cs new file mode 100644 index 0000000..e6fb165 --- /dev/null +++ b/tests/DatabaseBenchmark.Tests/Utils/StreamExtensions.cs @@ -0,0 +1,14 @@ +using System.IO; + +namespace DatabaseBenchmark.Tests.Utils +{ + public static class StreamExtensions + { + public static string ReadAsString(this Stream stream) + { + stream.Position = 0; + using var reader = new StreamReader(stream); + return reader.ReadToEnd(); + } + } +}