From 817a9f0f0ab09e1b76890af6d571ee3e948e94dd Mon Sep 17 00:00:00 2001 From: jkuehner Date: Mon, 3 Apr 2023 17:14:04 +0200 Subject: [PATCH] feat: support some linq string functions in where conditions, fixes #499 --- Client.Linq.Test/InfluxDBQueryVisitorTest.cs | 36 ++++++++ .../Internal/Expressions/StringFunction.cs | 58 ++++++++++++ Client.Linq/Internal/QueryAggregator.cs | 19 ++++ .../Internal/QueryExpressionTreeVisitor.cs | 88 +++++++++++++++++++ 4 files changed, 201 insertions(+) create mode 100644 Client.Linq/Internal/Expressions/StringFunction.cs diff --git a/Client.Linq.Test/InfluxDBQueryVisitorTest.cs b/Client.Linq.Test/InfluxDBQueryVisitorTest.cs index 93a5ccf1c..b86b58a3f 100644 --- a/Client.Linq.Test/InfluxDBQueryVisitorTest.cs +++ b/Client.Linq.Test/InfluxDBQueryVisitorTest.cs @@ -38,6 +38,42 @@ public void InitQueryApi() _queryApi = new Mock(options, queryService.Object, new FluxResultMapper()).Object; } + [Test] + public void StringFunctionsQuery() { + var query = from s in InfluxDBQueryable.Queryable("my-bucket", "my-org", _queryApi) + where s.SensorId.ToLower().Contains("aaa") + select s; + var visitor = BuildQueryVisitor(query); + + const string expected = "import \"strings\"\nstart_shifted = int(v: time(v: p2))\n\nfrom(bucket: p1) |> range(start: time(v: start_shifted)) |> filter(fn: (r) => strings.containsStr(v: strings.toLower(v: r[\"sensor_id\"]), substr: p3)) |> pivot(rowKey:[\"_time\"], columnKey: [\"_field\"], valueColumn: \"_value\") |> drop(columns: [\"_start\", \"_stop\", \"_measurement\"]) |> filter(fn: (r) => strings.containsStr(v: strings.toLower(v: r[\"sensor_id\"]), substr: p3))"; + var qry = visitor.BuildFluxQuery(); + Assert.AreEqual(expected, visitor.BuildFluxQuery()); + } + + [Test] + public void ToStringFunctionQuery() { + var query = from s in InfluxDBQueryable.Queryable("my-bucket", "my-org", _queryApi) + where s.Value.ToString() == "3" + select s; + var visitor = BuildQueryVisitor(query); + + const string expected = "start_shifted = int(v: time(v: p2))\n\nfrom(bucket: p1) |> range(start: time(v: start_shifted)) |> filter(fn: (r) => (string(v: r[\"data\"]) == p3)) |> pivot(rowKey:[\"_time\"], columnKey: [\"_field\"], valueColumn: \"_value\") |> drop(columns: [\"_start\", \"_stop\", \"_measurement\"]) |> filter(fn: (r) => (string(v: r[\"data\"]) == p3))"; + var qry = visitor.BuildFluxQuery(); + Assert.AreEqual(expected, visitor.BuildFluxQuery()); + } + + [Test] + public void ReplaceAllFunctionQuery() { + var query = from s in InfluxDBQueryable.Queryable("my-bucket", "my-org", _queryApi) + where s.SensorId.ToLower().Replace("a", "b") == "b" + select s; + var visitor = BuildQueryVisitor(query); + + const string expected = "import \"strings\"\nstart_shifted = int(v: time(v: p2))\n\nfrom(bucket: p1) |> range(start: time(v: start_shifted)) |> filter(fn: (r) => (strings.replaceAll(v: strings.toLower(v: r[\"sensor_id\"]), t: p3, u: p4) == p5)) |> pivot(rowKey:[\"_time\"], columnKey: [\"_field\"], valueColumn: \"_value\") |> drop(columns: [\"_start\", \"_stop\", \"_measurement\"]) |> filter(fn: (r) => (strings.replaceAll(v: strings.toLower(v: r[\"sensor_id\"]), t: p3, u: p4) == p5))"; + var qry = visitor.BuildFluxQuery(); + Assert.AreEqual(expected, visitor.BuildFluxQuery()); + } + [Test] public void DefaultQuery() { diff --git a/Client.Linq/Internal/Expressions/StringFunction.cs b/Client.Linq/Internal/Expressions/StringFunction.cs new file mode 100644 index 000000000..30de21c81 --- /dev/null +++ b/Client.Linq/Internal/Expressions/StringFunction.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using System.Text; + +namespace InfluxDB.Client.Linq.Internal.Expressions { + internal class StringFunction : IExpressionPart { + private readonly string functionName; + private readonly IEnumerable expressionParts; + private readonly IEnumerable expressionPartsPar2; + private readonly IEnumerable expressionPartsPar3; + + internal StringFunction(string functionName, IEnumerable expressionParts, IEnumerable expressionPartsPar2, IEnumerable expressionPartsPar3) { + this.functionName = functionName; + this.expressionParts = expressionParts; + this.expressionPartsPar2 = expressionPartsPar2; + this.expressionPartsPar3 = expressionPartsPar3; + } + + public void AppendFlux(StringBuilder builder) { + if (functionName != "string") + builder.Append("strings."); + builder.Append(functionName); + builder.Append("(v: "); + foreach (var expressionPart in expressionParts) { + expressionPart.AppendFlux(builder); + } + if (functionName == "containsStr") + { + builder.Append(", substr: "); + foreach (var expressionPart in expressionPartsPar2) { + expressionPart.AppendFlux(builder); + } + } + else if (functionName == "hasPrefix") { + builder.Append(", prefix: "); + foreach (var expressionPart in expressionPartsPar2) { + expressionPart.AppendFlux(builder); + } + } + else if (functionName == "hasSuffix") { + builder.Append(", suffix: "); + foreach (var expressionPart in expressionPartsPar2) { + expressionPart.AppendFlux(builder); + } + } + else if (functionName == "replaceAll") { + builder.Append(", t: "); + foreach (var expressionPart in expressionPartsPar2) { + expressionPart.AppendFlux(builder); + } + builder.Append(", u: "); + foreach (var expressionPart in expressionPartsPar3) { + expressionPart.AppendFlux(builder); + } + } + builder.Append(")"); + } + } +} \ No newline at end of file diff --git a/Client.Linq/Internal/QueryAggregator.cs b/Client.Linq/Internal/QueryAggregator.cs index fb5286c68..d71666db1 100644 --- a/Client.Linq/Internal/QueryAggregator.cs +++ b/Client.Linq/Internal/QueryAggregator.cs @@ -65,6 +65,7 @@ internal class QueryAggregator private ResultFunction _resultFunction; private readonly List _filterByTags; private readonly List _filterByFields; + private HashSet _imports; private readonly List<(string, string, bool, string)> _orders; private (string Every, string Period, string Fn)? _aggregateWindow; @@ -74,6 +75,7 @@ internal QueryAggregator() _limitTailNOffsetAssignments = new List(); _filterByTags = new List(); _filterByFields = new List(); + _imports = null; _orders = new List<(string, string, bool, string)>(); _aggregateWindow = null; } @@ -83,6 +85,15 @@ internal void AddBucket(string bucket) _bucketAssignment = bucket; } + internal void AddImport(string import) + { + if (_imports == null) + { + _imports = new HashSet(); + } + _imports.Add(import); + } + internal void AddRangeStart(string rangeStart, RangeExpressionType expressionType) { _rangeStartAssignment = rangeStart; @@ -219,6 +230,7 @@ internal string BuildFluxQuery(QueryableOptimizerSettings settings) var query = new StringBuilder(); + query.Append(BuildImports()); query.Append(JoinList(transforms, "\n")); query.Append("\n\n"); query.Append(JoinList(parts, " |> ")); @@ -334,6 +346,13 @@ private string BuildFilter(IEnumerable filterBy) return filter.ToString(); } + private string BuildImports() + { + if (_imports != null) + return string.Join("", _imports.Select(x => "import \"" + x + "\"\n")); + return string.Empty; + } + private string BuildOperator(string operatorName, params object[] variables) { var builderVariables = new StringBuilder(); diff --git a/Client.Linq/Internal/QueryExpressionTreeVisitor.cs b/Client.Linq/Internal/QueryExpressionTreeVisitor.cs index d1f7816b4..37629e092 100644 --- a/Client.Linq/Internal/QueryExpressionTreeVisitor.cs +++ b/Client.Linq/Internal/QueryExpressionTreeVisitor.cs @@ -187,6 +187,84 @@ protected override Expression VisitMethodCall(MethodCallExpression expression) return expression; } + if (expression.Method.DeclaringType == typeof(string)) + { + if (expression.Method.Name.Equals("ToLower")) + { + _context.QueryAggregator.AddImport("strings"); + var partsCount = this._expressionParts.Count; + this.Visit(expression.Object); + var part = this.GetAndRemoveExpressionParts(partsCount, this._expressionParts.Count); + this._expressionParts.Add(new StringFunction("toLower", part, null, null)); + return expression; + } + else if (expression.Method.Name.Equals("ToUpper")) + { + _context.QueryAggregator.AddImport("strings"); + var partsCount = this._expressionParts.Count; + this.Visit(expression.Object); + var part = this.GetAndRemoveExpressionParts(partsCount, this._expressionParts.Count); + this._expressionParts.Add(new StringFunction("toUpper", part, null, null)); + return expression; + } + else if (expression.Method.Name.Equals("Contains")) + { + _context.QueryAggregator.AddImport("strings"); + var partsCount = this._expressionParts.Count; + this.Visit(expression.Object); + var part = this.GetAndRemoveExpressionParts(partsCount, this._expressionParts.Count); + partsCount = this._expressionParts.Count; + this.Visit(expression.Arguments[0]); + var part2 = this.GetAndRemoveExpressionParts(partsCount, this._expressionParts.Count); + this._expressionParts.Add(new StringFunction("containsStr", part, part2, null)); + return expression; + } + else if (expression.Method.Name.Equals("StartsWith")) { + _context.QueryAggregator.AddImport("strings"); + var partsCount = this._expressionParts.Count; + this.Visit(expression.Object); + var part = this.GetAndRemoveExpressionParts(partsCount, this._expressionParts.Count); + partsCount = this._expressionParts.Count; + this.Visit(expression.Arguments[0]); + var part2 = this.GetAndRemoveExpressionParts(partsCount, this._expressionParts.Count); + this._expressionParts.Add(new StringFunction("hasPrefix", part, part2, null)); + return expression; + } + else if (expression.Method.Name.Equals("EndsWith")) { + _context.QueryAggregator.AddImport("strings"); + var partsCount = this._expressionParts.Count; + this.Visit(expression.Object); + var part = this.GetAndRemoveExpressionParts(partsCount, this._expressionParts.Count); + partsCount = this._expressionParts.Count; + this.Visit(expression.Arguments[0]); + var part2 = this.GetAndRemoveExpressionParts(partsCount, this._expressionParts.Count); + this._expressionParts.Add(new StringFunction("hasSuffix", part, part2, null)); + return expression; + } + else if (expression.Method.Name.Equals("Replace")) { + _context.QueryAggregator.AddImport("strings"); + var partsCount = this._expressionParts.Count; + this.Visit(expression.Object); + var part = this.GetAndRemoveExpressionParts(partsCount, this._expressionParts.Count); + partsCount = this._expressionParts.Count; + this.Visit(expression.Arguments[0]); + var part2 = this.GetAndRemoveExpressionParts(partsCount, this._expressionParts.Count); + partsCount = this._expressionParts.Count; + this.Visit(expression.Arguments[1]); + var part3 = this.GetAndRemoveExpressionParts(partsCount, this._expressionParts.Count); + this._expressionParts.Add(new StringFunction("replaceAll", part, part2, part3)); + return expression; + } + } + + if (expression.Method.Name.Equals("ToString")) { + var partsCount = this._expressionParts.Count; + this.Visit(expression.Object); + var part = this.GetAndRemoveExpressionParts(partsCount, this._expressionParts.Count); + this._expressionParts.Add(new StringFunction("string", part, null, null)); + return expression; + } + return base.VisitMethodCall(expression); } @@ -294,6 +372,16 @@ private void NormalizeNamedFieldValue() NormalizeNamedFieldValue(); } + private IEnumerable GetAndRemoveExpressionParts(int start, int end) + { + var parts = this._expressionParts.GetRange(start, end - start); + for (int i = start; i < end; i++) + { + this._expressionParts.RemoveAt(i); + } + return parts; + } + /// /// Mark variables that are use to filter by tag by tag as tag. ///