diff --git a/CHANGELOG.md b/CHANGELOG.md index ee770fdfc..edddfa362 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,10 @@ ### Features 1. [#304](https://github.com/influxdata/influxdb-client-csharp/pull/304): Add `InvocableScriptsApi` to create, update, list, delete and invoke scripts by seamless way +1. [#308](https://github.com/influxdata/influxdb-client-csharp/pull/308): Add support for `TakeLast` expression [LINQ] ### Bug Fixes 1. [#305](https://github.com/influxdata/influxdb-client-csharp/pull/305): Authentication Cookies follow redirects - -### Bug Fixes 1. [#309](https://github.com/influxdata/influxdb-client-csharp/pull/309): Query expression for joins of binary operators [LINQ] ## 4.0.0 [2022-03-18] diff --git a/Client.Linq.Test/InfluxDBQueryVisitorTest.cs b/Client.Linq.Test/InfluxDBQueryVisitorTest.cs index 94f2dca12..0d01d2cac 100644 --- a/Client.Linq.Test/InfluxDBQueryVisitorTest.cs +++ b/Client.Linq.Test/InfluxDBQueryVisitorTest.cs @@ -15,7 +15,6 @@ using NUnit.Framework; using Remotion.Linq; using Remotion.Linq.Parsing.ExpressionVisitors; -using Remotion.Linq.Parsing.Structure; using Expression = System.Linq.Expressions.Expression; namespace Client.Linq.Test @@ -100,17 +99,32 @@ public void ResultOperatorTake() } [Test] - public void ResultOperatorSkip() + public void ResultOperatorTakeLast() { var query = from s in InfluxDBQueryable.Queryable("my-bucket", "my-org", _queryApi) select s; - var visitor = BuildQueryVisitor(query, MakeExpression(query, q => q.Skip(5))); - const string expected = FluxStart; + var visitor = BuildQueryVisitor(query, MakeExpression(query, q => q.TakeLast(10))); + + var expected = FluxStart + " " + "|> tail(n: p3)"; + Assert.AreEqual(expected, visitor.BuildFluxQuery()); + + visitor = BuildQueryVisitor(query, MakeExpression(query, q => q.TakeLast(10).Skip(5))); + expected = FluxStart + " " + "|> tail(n: p3, offset: p4)"; Assert.AreEqual(expected, visitor.BuildFluxQuery()); } + [Test] + public void ResultOperatorSkip() + { + var query = from s in InfluxDBQueryable.Queryable("my-bucket", "my-org", _queryApi) + select s; + var visitor = BuildQueryVisitor(query, MakeExpression(query, q => q.Skip(5))); + + Assert.AreEqual(FluxStart, visitor.BuildFluxQuery()); + } + [Test] public void ResultOperatorTakeSkip() { @@ -1097,7 +1111,8 @@ public void FilterByTimeAndTagWithAnds() private InfluxDBQueryVisitor BuildQueryVisitor(IQueryable queryable, Expression expression = null) { var queryExecutor = (InfluxDBQueryExecutor)((DefaultQueryProvider)queryable.Provider).Executor; - var queryModel = QueryParser.CreateDefault().GetParsedQuery(expression ?? queryable.Expression); + var queryModel = InfluxDBQueryable.CreateQueryParser() + .GetParsedQuery(expression ?? queryable.Expression); return queryExecutor.QueryVisitor(queryModel); } diff --git a/Client.Linq/InfluxDBQueryable.cs b/Client.Linq/InfluxDBQueryable.cs index 4c277308e..bccaf15ae 100644 --- a/Client.Linq/InfluxDBQueryable.cs +++ b/Client.Linq/InfluxDBQueryable.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Linq.Expressions; using System.Threading; using InfluxDB.Client.Core; using InfluxDB.Client.Linq.Internal; +using InfluxDB.Client.Linq.Internal.NodeTypes; using Remotion.Linq; using Remotion.Linq.Parsing.Structure; +using Remotion.Linq.Parsing.Structure.NodeTypeProviders; +using Expression = System.Linq.Expressions.Expression; namespace InfluxDB.Client.Linq { @@ -213,9 +215,12 @@ private static IQueryExecutor CreateExecutor(string bucket, string org, QueryApi queryableOptimizerSettings ?? new QueryableOptimizerSettings()); } - private static QueryParser CreateQueryParser() + internal static QueryParser CreateQueryParser() { - return QueryParser.CreateDefault(); + var queryParser = QueryParser.CreateDefault(); + var compoundNodeTypeProvider = queryParser.NodeTypeProvider as CompoundNodeTypeProvider; + compoundNodeTypeProvider?.InnerProviders.Add(new InfluxDBNodeTypeProvider()); + return queryParser; } public IAsyncEnumerable GetAsyncEnumerator(CancellationToken cancellationToken = default) diff --git a/Client.Linq/Internal/NodeTypes/InfluxDBNodeTypeProvider.cs b/Client.Linq/Internal/NodeTypes/InfluxDBNodeTypeProvider.cs new file mode 100644 index 000000000..56cfefb4c --- /dev/null +++ b/Client.Linq/Internal/NodeTypes/InfluxDBNodeTypeProvider.cs @@ -0,0 +1,27 @@ +using System; +using System.Reflection; +using Remotion.Linq.Parsing.Structure; +using Remotion.Linq.Parsing.Structure.NodeTypeProviders; + +namespace InfluxDB.Client.Linq.Internal.NodeTypes +{ + internal class InfluxDBNodeTypeProvider : INodeTypeProvider + { + private readonly MethodInfoBasedNodeTypeRegistry _methodInfoRegistry = new MethodInfoBasedNodeTypeRegistry(); + + internal InfluxDBNodeTypeProvider() + { + _methodInfoRegistry.Register(TakeLastExpressionNode.GetSupportedMethods, typeof(TakeLastExpressionNode)); + } + + public bool IsRegistered(MethodInfo method) + { + return _methodInfoRegistry.IsRegistered(method); + } + + public Type GetNodeType(MethodInfo method) + { + return _methodInfoRegistry.GetNodeType(method); + } + } +} \ No newline at end of file diff --git a/Client.Linq/Internal/NodeTypes/TakeLastNodeType.cs b/Client.Linq/Internal/NodeTypes/TakeLastNodeType.cs new file mode 100644 index 000000000..5ab4e6e23 --- /dev/null +++ b/Client.Linq/Internal/NodeTypes/TakeLastNodeType.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using InfluxDB.Client.Core; +using Remotion.Linq.Clauses; +using Remotion.Linq.Clauses.ResultOperators; +using Remotion.Linq.Clauses.StreamedData; +using Remotion.Linq.Parsing.Structure.IntermediateModel; + +namespace InfluxDB.Client.Linq.Internal.NodeTypes +{ + internal class TakeLastExpressionNode : ResultOperatorExpressionNodeBase + { + private readonly Expression _count; + + internal static readonly IEnumerable GetSupportedMethods = + new ReadOnlyCollection(typeof(Enumerable).GetRuntimeMethods() + .Concat(typeof(Queryable).GetRuntimeMethods()).ToList()) + .Where(mi => mi.Name == "TakeLast"); + + public TakeLastExpressionNode(MethodCallExpressionParseInfo parseInfo, Expression count) + : base(parseInfo, null, null) + { + _count = count; + } + + public override Expression Resolve( + ParameterExpression inputParameter, + Expression expressionToBeResolved, + ClauseGenerationContext clauseGenerationContext) + { + Arguments.CheckNotNull(inputParameter, nameof(inputParameter)); + Arguments.CheckNotNull(expressionToBeResolved, nameof(expressionToBeResolved)); + return Source.Resolve(inputParameter, expressionToBeResolved, clauseGenerationContext); + } + + protected override ResultOperatorBase CreateResultOperator(ClauseGenerationContext clauseGenerationContext) + { + return new TakeLastResultOperator(_count); + } + } + + internal class TakeLastResultOperator : SequenceTypePreservingResultOperatorBase + { + private Expression _count; + + internal TakeLastResultOperator(Expression count) + { + Arguments.CheckNotNull(count, nameof(count)); + Count = count; + } + + public Expression Count + { + get => _count; + private set + { + Arguments.CheckNotNull(value, nameof(value)); + _count = ReferenceEquals(value.Type, typeof(int)) + ? value + : throw new ArgumentException(string.Format( + "The value expression returns '{0}', an expression returning 'System.Int32' was expected.", + new object[] + { + value.Type + }), nameof(value)); + } + } + + public override ResultOperatorBase Clone(CloneContext cloneContext) + { + return new TakeResultOperator(Count); + } + + public override StreamedSequence ExecuteInMemory(StreamedSequence input) + { + return new StreamedSequence( + input.GetTypedSequence().Take(GetConstantCount()).AsQueryable(), + GetOutputDataInfo(input.DataInfo)); + } + + public override void TransformExpressions(Func transformation) + { + Arguments.CheckNotNull(transformation, nameof(transformation)); + Count = transformation(Count); + } + + public override string ToString() + { + return $"TakeLast({Count})"; + } + + private int GetConstantCount() + { + return GetConstantValueFromExpression("count", Count); + } + } +} \ No newline at end of file diff --git a/Client.Linq/Internal/QueryAggregator.cs b/Client.Linq/Internal/QueryAggregator.cs index 5658b05f8..02acd5ff0 100644 --- a/Client.Linq/Internal/QueryAggregator.cs +++ b/Client.Linq/Internal/QueryAggregator.cs @@ -49,6 +49,7 @@ internal enum RangeExpressionType internal class LimitOffsetAssignment { + internal string FluxFunction; internal string N; internal string Offset; } @@ -60,7 +61,7 @@ internal class QueryAggregator private RangeExpressionType _rangeStartExpression; private string _rangeStopAssignment; private RangeExpressionType _rangeStopExpression; - private readonly List _limitNOffsetAssignments; + private readonly List _limitTailNOffsetAssignments; private ResultFunction _resultFunction; private readonly List _filterByTags; private readonly List _filterByFields; @@ -70,7 +71,7 @@ internal class QueryAggregator internal QueryAggregator() { _resultFunction = ResultFunction.None; - _limitNOffsetAssignments = new List(); + _limitTailNOffsetAssignments = new List(); _filterByTags = new List(); _filterByFields = new List(); _orders = new List<(string, string, bool, string)>(); @@ -100,27 +101,29 @@ internal void AddAggregateWindow(string everyVariable, string periodVariable, st } - internal void AddLimitN(string limitNAssignment) + internal void AddLimitTailN(string limitNAssignment, string fluxFunction) { - if (_limitNOffsetAssignments.Count > 0 && _limitNOffsetAssignments.Last().N == null) + if (_limitTailNOffsetAssignments.Count > 0 && _limitTailNOffsetAssignments.Last().N == null) { - _limitNOffsetAssignments.Last().N = limitNAssignment; + _limitTailNOffsetAssignments.Last().FluxFunction = fluxFunction; + _limitTailNOffsetAssignments.Last().N = limitNAssignment; } else { - _limitNOffsetAssignments.Add(new LimitOffsetAssignment { N = limitNAssignment }); + _limitTailNOffsetAssignments.Add(new LimitOffsetAssignment + { FluxFunction = fluxFunction, N = limitNAssignment }); } } - internal void AddLimitOffset(string limitOffsetAssignment) + internal void AddLimitTailOffset(string limitOffsetAssignment) { - if (_limitNOffsetAssignments.Count > 0) + if (_limitTailNOffsetAssignments.Count > 0) { - _limitNOffsetAssignments.Last().Offset = limitOffsetAssignment; + _limitTailNOffsetAssignments.Last().Offset = limitOffsetAssignment; } else { - _limitNOffsetAssignments.Add(new LimitOffsetAssignment { Offset = limitOffsetAssignment }); + _limitTailNOffsetAssignments.Add(new LimitOffsetAssignment { Offset = limitOffsetAssignment }); } } @@ -193,10 +196,10 @@ internal string BuildFluxQuery(QueryableOptimizerSettings settings) } // https://docs.influxdata.com/flux/v0.x/stdlib/universe/limit/ - foreach (var limitNOffsetAssignment in _limitNOffsetAssignments) + foreach (var limitNOffsetAssignment in _limitTailNOffsetAssignments) if (limitNOffsetAssignment.N != null) { - parts.Add(BuildOperator("limit", + parts.Add(BuildOperator(limitNOffsetAssignment.FluxFunction, "n", limitNOffsetAssignment.N, "offset", limitNOffsetAssignment.Offset)); } diff --git a/Client.Linq/Internal/QueryVisitor.cs b/Client.Linq/Internal/QueryVisitor.cs index 51e42d4d0..375c9f09c 100644 --- a/Client.Linq/Internal/QueryVisitor.cs +++ b/Client.Linq/Internal/QueryVisitor.cs @@ -6,6 +6,7 @@ using System.Text; using InfluxDB.Client.Api.Domain; using InfluxDB.Client.Linq.Internal.Expressions; +using InfluxDB.Client.Linq.Internal.NodeTypes; using Remotion.Linq; using Remotion.Linq.Clauses; using Remotion.Linq.Clauses.ResultOperators; @@ -135,12 +136,17 @@ public override void VisitResultOperator(ResultOperatorBase resultOperator, Quer { case TakeResultOperator takeResultOperator: var takeVariable = GetFluxExpression(takeResultOperator.Count, takeResultOperator); - _context.QueryAggregator.AddLimitN(takeVariable); + _context.QueryAggregator.AddLimitTailN(takeVariable, "limit"); + break; + + case TakeLastResultOperator takeLastResultOperator: + var takeLastVariable = GetFluxExpression(takeLastResultOperator.Count, takeLastResultOperator); + _context.QueryAggregator.AddLimitTailN(takeLastVariable, "tail"); break; case SkipResultOperator skipResultOperator: var skipVariable = GetFluxExpression(skipResultOperator.Count, skipResultOperator); - _context.QueryAggregator.AddLimitOffset(skipVariable); + _context.QueryAggregator.AddLimitTailOffset(skipVariable); break; case AnyResultOperator _: diff --git a/Client.Linq/README.md b/Client.Linq/README.md index 3f989455e..96609083f 100644 --- a/Client.Linq/README.md +++ b/Client.Linq/README.md @@ -32,6 +32,7 @@ This section contains links to the client library documentation. - [Or](#or) - [Any](#any) - [Take](#take) + - [TakeLast](#takelast) - [Skip](#skip) - [OrderBy](#orderby) - [Count](#count) @@ -850,6 +851,24 @@ from(bucket: "my-bucket") |> limit(n: 10) ``` +### TakeLast + +```c# +var query = (from s in InfluxDBQueryable.Queryable("my-bucket", "my-org", queryApi) + select s) + .TakeLast(10); +``` + +Flux Query: + +```flux +from(bucket: "my-bucket") + |> range(start: 0) + |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value") + |> drop(columns: ["_start", "_stop", "_measurement"]) + |> tail(n: 10) +``` + ### Skip ```c#