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/StringFunction.cs b/Client.Linq/Internal/Expressions/StringFunction.cs new file mode 100644 index 000000000..4df535a06 --- /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..4fc4d73df 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,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 +231,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 +347,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..c9d5c4433 100644 --- a/Client.Linq/Internal/QueryExpressionTreeVisitor.cs +++ b/Client.Linq/Internal/QueryExpressionTreeVisitor.cs @@ -187,6 +187,88 @@ 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 = _expressionParts.Count; + Visit(expression.Object); + var part = GetAndRemoveExpressionParts(partsCount, _expressionParts.Count); + _expressionParts.Add(new StringFunction("toLower", part, null, null)); + return expression; + } + else if (expression.Method.Name.Equals("ToUpper")) + { + _context.QueryAggregator.AddImport("strings"); + var partsCount = _expressionParts.Count; + Visit(expression.Object); + var part = GetAndRemoveExpressionParts(partsCount, _expressionParts.Count); + _expressionParts.Add(new StringFunction("toUpper", part, null, null)); + return expression; + } + else if (expression.Method.Name.Equals("Contains")) + { + _context.QueryAggregator.AddImport("strings"); + var partsCount = _expressionParts.Count; + Visit(expression.Object); + var part = GetAndRemoveExpressionParts(partsCount, _expressionParts.Count); + partsCount = _expressionParts.Count; + Visit(expression.Arguments[0]); + var part2 = GetAndRemoveExpressionParts(partsCount, _expressionParts.Count); + _expressionParts.Add(new StringFunction("containsStr", part, part2, null)); + return expression; + } + else if (expression.Method.Name.Equals("StartsWith")) + { + _context.QueryAggregator.AddImport("strings"); + var partsCount = _expressionParts.Count; + Visit(expression.Object); + var part = GetAndRemoveExpressionParts(partsCount, _expressionParts.Count); + partsCount = _expressionParts.Count; + Visit(expression.Arguments[0]); + var part2 = GetAndRemoveExpressionParts(partsCount, _expressionParts.Count); + _expressionParts.Add(new StringFunction("hasPrefix", part, part2, null)); + return expression; + } + else if (expression.Method.Name.Equals("EndsWith")) + { + _context.QueryAggregator.AddImport("strings"); + var partsCount = _expressionParts.Count; + Visit(expression.Object); + var part = GetAndRemoveExpressionParts(partsCount, _expressionParts.Count); + partsCount = _expressionParts.Count; + Visit(expression.Arguments[0]); + var part2 = GetAndRemoveExpressionParts(partsCount, _expressionParts.Count); + _expressionParts.Add(new StringFunction("hasSuffix", part, part2, null)); + return expression; + } + else if (expression.Method.Name.Equals("Replace")) + { + _context.QueryAggregator.AddImport("strings"); + var partsCount = _expressionParts.Count; + Visit(expression.Object); + var part = GetAndRemoveExpressionParts(partsCount, _expressionParts.Count); + partsCount = _expressionParts.Count; + Visit(expression.Arguments[0]); + var part2 = GetAndRemoveExpressionParts(partsCount, _expressionParts.Count); + partsCount = _expressionParts.Count; + Visit(expression.Arguments[1]); + var part3 = GetAndRemoveExpressionParts(partsCount, _expressionParts.Count); + _expressionParts.Add(new StringFunction("replaceAll", part, part2, part3)); + return expression; + } + } + + if (expression.Method.Name.Equals("ToString")) + { + var partsCount = _expressionParts.Count; + Visit(expression.Object); + var part = GetAndRemoveExpressionParts(partsCount, _expressionParts.Count); + _expressionParts.Add(new StringFunction("string", part, null, null)); + return expression; + } + return base.VisitMethodCall(expression); } @@ -294,6 +376,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. ///