From fb8aeff7fedad0bf4611aea85c3150f0a7e8f802 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 --- CHANGELOG.md | 3 +- Client.Linq.Test/InfluxDBQueryVisitorTest.cs | 42 ++++++++++++ .../Expressions/String/ContainsStr.cs | 26 ++++++++ .../Internal/Expressions/String/HasPrefix.cs | 26 ++++++++ .../Internal/Expressions/String/HasSuffix.cs | 26 ++++++++ .../Internal/Expressions/String/ReplaceAll.cs | 31 +++++++++ .../Internal/Expressions/String/ToLower.cs | 22 +++++++ .../Internal/Expressions/String/ToString.cs | 22 +++++++ .../Internal/Expressions/String/ToUpper.cs | 22 +++++++ Client.Linq/Internal/QueryAggregator.cs | 22 +++++++ .../Internal/QueryExpressionTreeVisitor.cs | 64 +++++++++++++++++++ 11 files changed, 305 insertions(+), 1 deletion(-) create mode 100644 Client.Linq/Internal/Expressions/String/ContainsStr.cs create mode 100644 Client.Linq/Internal/Expressions/String/HasPrefix.cs create mode 100644 Client.Linq/Internal/Expressions/String/HasSuffix.cs create mode 100644 Client.Linq/Internal/Expressions/String/ReplaceAll.cs create mode 100644 Client.Linq/Internal/Expressions/String/ToLower.cs create mode 100644 Client.Linq/Internal/Expressions/String/ToString.cs create mode 100644 Client.Linq/Internal/Expressions/String/ToUpper.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 41b6190fd..acd279450 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ ## 4.13.0 [unreleased] ### Features -1. [#528](https://github.com/influxdata/influxdb-client-csharp/pull/528): Add HttpClient as a part of InfluxDBClientOptions + 1. [#528](https://github.com/influxdata/influxdb-client-csharp/pull/528): Add HttpClient as a part of InfluxDBClientOptions + 1. [#500](https://github.com/influxdata/influxdb-client-csharp/pull/500): Support String functions in Linq ### Dependencies Update dependencies: diff --git a/Client.Linq.Test/InfluxDBQueryVisitorTest.cs b/Client.Linq.Test/InfluxDBQueryVisitorTest.cs index 12ca8160e..6202d2c41 100644 --- a/Client.Linq.Test/InfluxDBQueryVisitorTest.cs +++ b/Client.Linq.Test/InfluxDBQueryVisitorTest.cs @@ -38,6 +38,48 @@ 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/String/ContainsStr.cs b/Client.Linq/Internal/Expressions/String/ContainsStr.cs new file mode 100644 index 000000000..4f6a0a759 --- /dev/null +++ b/Client.Linq/Internal/Expressions/String/ContainsStr.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Text; + +namespace InfluxDB.Client.Linq.Internal.Expressions.String +{ + internal class ContainsStr : IExpressionPart + { + private readonly IEnumerable _value; + private readonly IEnumerable _substr; + + internal ContainsStr(IEnumerable value, IEnumerable substr) + { + this._value = value; + this._substr = substr; + } + + public void AppendFlux(StringBuilder builder) + { + builder.Append("strings.containsStr(v: "); + foreach (var expressionPart in _value) expressionPart.AppendFlux(builder); + builder.Append(", substr: "); + foreach (var expressionPart in _substr) expressionPart.AppendFlux(builder); + builder.Append(")"); + } + } +} \ No newline at end of file diff --git a/Client.Linq/Internal/Expressions/String/HasPrefix.cs b/Client.Linq/Internal/Expressions/String/HasPrefix.cs new file mode 100644 index 000000000..4cf99c889 --- /dev/null +++ b/Client.Linq/Internal/Expressions/String/HasPrefix.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Text; + +namespace InfluxDB.Client.Linq.Internal.Expressions.String +{ + internal class HasPrefix : IExpressionPart + { + private readonly IEnumerable _value; + private readonly IEnumerable _prefix; + + internal HasPrefix(IEnumerable value, IEnumerable prefix) + { + this._value = value; + this._prefix = prefix; + } + + public void AppendFlux(StringBuilder builder) + { + builder.Append("strings.hasPrefix(v: "); + foreach (var expressionPart in _value) expressionPart.AppendFlux(builder); + builder.Append(", prefix: "); + foreach (var expressionPart in _prefix) expressionPart.AppendFlux(builder); + builder.Append(")"); + } + } +} \ No newline at end of file diff --git a/Client.Linq/Internal/Expressions/String/HasSuffix.cs b/Client.Linq/Internal/Expressions/String/HasSuffix.cs new file mode 100644 index 000000000..dedc45296 --- /dev/null +++ b/Client.Linq/Internal/Expressions/String/HasSuffix.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Text; + +namespace InfluxDB.Client.Linq.Internal.Expressions.String +{ + internal class HasSuffix : IExpressionPart + { + private readonly IEnumerable _value; + private readonly IEnumerable _suffix; + + internal HasSuffix(IEnumerable value, IEnumerable suffix) + { + this._value = value; + this._suffix = suffix; + } + + public void AppendFlux(StringBuilder builder) + { + builder.Append("strings.hasSuffix(v: "); + foreach (var expressionPart in _value) expressionPart.AppendFlux(builder); + builder.Append(", suffix: "); + foreach (var expressionPart in _suffix) expressionPart.AppendFlux(builder); + builder.Append(")"); + } + } +} \ No newline at end of file diff --git a/Client.Linq/Internal/Expressions/String/ReplaceAll.cs b/Client.Linq/Internal/Expressions/String/ReplaceAll.cs new file mode 100644 index 000000000..cd28f91db --- /dev/null +++ b/Client.Linq/Internal/Expressions/String/ReplaceAll.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Text; + +namespace InfluxDB.Client.Linq.Internal.Expressions.String +{ + internal class ReplaceAll : IExpressionPart + { + private readonly IEnumerable _value; + private readonly IEnumerable _substring; + private readonly IEnumerable _replacement; + + internal ReplaceAll(IEnumerable value, IEnumerable substring, + IEnumerable replacement) + { + this._value = value; + this._substring = substring; + this._replacement = replacement; + } + + public void AppendFlux(StringBuilder builder) + { + builder.Append("strings.replaceAll(v: "); + foreach (var expressionPart in _value) expressionPart.AppendFlux(builder); + builder.Append(", t: "); + foreach (var expressionPart in _substring) expressionPart.AppendFlux(builder); + builder.Append(", u: "); + foreach (var expressionPart in _replacement) expressionPart.AppendFlux(builder); + builder.Append(")"); + } + } +} \ No newline at end of file diff --git a/Client.Linq/Internal/Expressions/String/ToLower.cs b/Client.Linq/Internal/Expressions/String/ToLower.cs new file mode 100644 index 000000000..15128aecb --- /dev/null +++ b/Client.Linq/Internal/Expressions/String/ToLower.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Text; + +namespace InfluxDB.Client.Linq.Internal.Expressions.String +{ + internal class ToLower : IExpressionPart + { + private readonly IEnumerable _value; + + internal ToLower(IEnumerable value) + { + this._value = value; + } + + public void AppendFlux(StringBuilder builder) + { + builder.Append("strings.toLower(v: "); + foreach (var expressionPart in _value) expressionPart.AppendFlux(builder); + builder.Append(")"); + } + } +} \ No newline at end of file diff --git a/Client.Linq/Internal/Expressions/String/ToString.cs b/Client.Linq/Internal/Expressions/String/ToString.cs new file mode 100644 index 000000000..f64a78fed --- /dev/null +++ b/Client.Linq/Internal/Expressions/String/ToString.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Text; + +namespace InfluxDB.Client.Linq.Internal.Expressions.String +{ + internal class ToString : IExpressionPart + { + private readonly IEnumerable _value; + + internal ToString(IEnumerable value) + { + this._value = value; + } + + public void AppendFlux(StringBuilder builder) + { + builder.Append("string(v: "); + foreach (var expressionPart in _value) expressionPart.AppendFlux(builder); + builder.Append(")"); + } + } +} \ No newline at end of file diff --git a/Client.Linq/Internal/Expressions/String/ToUpper.cs b/Client.Linq/Internal/Expressions/String/ToUpper.cs new file mode 100644 index 000000000..065283e92 --- /dev/null +++ b/Client.Linq/Internal/Expressions/String/ToUpper.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Text; + +namespace InfluxDB.Client.Linq.Internal.Expressions.String +{ + internal class ToUpper : IExpressionPart + { + private readonly IEnumerable _value; + + internal ToUpper(IEnumerable value) + { + this._value = value; + } + + public void AppendFlux(StringBuilder builder) + { + builder.Append("strings.toUpper(v: "); + foreach (var expressionPart in _value) 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..9071b30d8 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; @@ -83,6 +84,16 @@ 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,16 @@ 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 f3a4a65ca..16eccfac8 100644 --- a/Client.Linq/Internal/QueryExpressionTreeVisitor.cs +++ b/Client.Linq/Internal/QueryExpressionTreeVisitor.cs @@ -5,6 +5,7 @@ using InfluxDB.Client.Api.Domain; using InfluxDB.Client.Core; using InfluxDB.Client.Linq.Internal.Expressions; +using InfluxDB.Client.Linq.Internal.Expressions.String; using Remotion.Linq.Clauses; using Remotion.Linq.Clauses.Expressions; using Remotion.Linq.Clauses.ResultOperators; @@ -187,6 +188,62 @@ protected override Expression VisitMethodCall(MethodCallExpression expression) return expression; } + if (expression.Method.DeclaringType == typeof(string)) + { + _context.QueryAggregator.AddImport("strings"); + var partsCount = _expressionParts.Count; + Visit(expression.Object); + var part = GetAndRemoveExpressionParts(partsCount, _expressionParts.Count); + IEnumerable part2 = null; + IEnumerable part3 = null; + if (expression.Arguments.Count > 0) + { + partsCount = _expressionParts.Count; + Visit(expression.Arguments[0]); + part2 = GetAndRemoveExpressionParts(partsCount, _expressionParts.Count); + } + + if (expression.Arguments.Count > 1) + { + partsCount = _expressionParts.Count; + Visit(expression.Arguments[1]); + part3 = GetAndRemoveExpressionParts(partsCount, _expressionParts.Count); + } + + switch (expression.Method.Name) + { + case "ToLower": + _expressionParts.Add(new ToLower(part)); + return expression; + case "ToUpper": + _expressionParts.Add(new ToUpper(part)); + return expression; + case "Contains": + _expressionParts.Add(new ContainsStr(part, part2)); + return expression; + case "StartsWith": + _expressionParts.Add(new HasPrefix(part, part2)); + return expression; + case "EndsWith": + _expressionParts.Add(new HasSuffix(part, part2)); + return expression; + case "Replace": + _expressionParts.Add(new ReplaceAll(part, part2, part3)); + return expression; + } + + throw new NotSupportedException(expression.Method.Name + " of String class is not yet supported."); + } + + if (expression.Method.Name.Equals("ToString")) + { + var partsCount = _expressionParts.Count; + Visit(expression.Object); + var part = GetAndRemoveExpressionParts(partsCount, _expressionParts.Count); + _expressionParts.Add(new ToString(part)); + return expression; + } + return base.VisitMethodCall(expression); } @@ -294,6 +351,13 @@ private void NormalizeNamedFieldValue() NormalizeNamedFieldValue(); } + private IEnumerable GetAndRemoveExpressionParts(int start, int end) + { + var parts = _expressionParts.GetRange(start, end - start); + for (var i = start; i < end; i++) _expressionParts.RemoveAt(i); + return parts; + } + /// /// Mark variables that are use to filter by tag by tag as tag. ///