From 8339edeb5d86eb310ad33c4c63e610bcadcbe2f7 Mon Sep 17 00:00:00 2001 From: MikaelGRA Date: Thu, 22 Jun 2017 22:46:59 +0200 Subject: [PATCH] bumped version, improved aggregate support --- src/Vibrant.InfluxDB.Client/InfluxClient.cs | 59 ++- .../InfluxComputedAttribute.cs | 33 ++ .../InfluxQueryOptions.cs | 14 + .../Metadata/DatabaseMeasurementInfo.cs | 6 +- .../Metadata/InfluxRowTypeInfo.cs | 3 + .../Metadata/MetadataCache.cs | 20 +- .../Parsers/ResultSetFactory.cs | 50 +- .../Resources/Errors.cs | 3 +- .../Vibrant.InfluxDB.Client.csproj | 3 +- .../ComputerInfo.cs | 11 + .../InfluxClientFixture.cs | 254 ++++----- .../ReadWriteTests.cs | 492 +++++++++--------- 12 files changed, 509 insertions(+), 439 deletions(-) create mode 100644 src/Vibrant.InfluxDB.Client/InfluxComputedAttribute.cs diff --git a/src/Vibrant.InfluxDB.Client/InfluxClient.cs b/src/Vibrant.InfluxDB.Client/InfluxClient.cs index 614b9c5..a2ac62a 100644 --- a/src/Vibrant.InfluxDB.Client/InfluxClient.cs +++ b/src/Vibrant.InfluxDB.Client/InfluxClient.cs @@ -84,25 +84,25 @@ public InfluxClient( Uri endpoint ) /// Executes an arbitrary command that returns a table as a result. /// /// - /// + /// /// /// - public Task> ExecuteOperationAsync( string commandOrQuery, string db ) + public Task> ExecuteOperationAsync( string command, string db ) where TInfluxRow : new() { - return ExecuteQueryInternalAsync( commandOrQuery, db ); + return ExecuteQueryInternalAsync( command, db ); } /// /// Executes an arbitrary command or query that returns a table as a result. /// /// - /// + /// /// - public Task> ExecuteOperationAsync( string commandOrQuery ) + public Task> ExecuteOperationAsync( string command ) where TInfluxRow : new() { - return ExecuteQueryInternalAsync( commandOrQuery ); + return ExecuteQueryInternalAsync( command ); } /// @@ -798,38 +798,37 @@ public Task DeleteRangeAsync( string db, string measurementName, DateTime from, #endregion - internal async Task GetMetaInformationAsync( string db, string measurementName, bool forceRefresh ) + internal async Task GetMetaInformationAsync( string db, string measurementName, TimeSpan? expiration ) { var key = new DatabaseMeasurementInfoKey( db, measurementName ); DatabaseMeasurementInfo info; - if( !forceRefresh ) + lock( _seriesMetaCache ) { - lock( _seriesMetaCache ) - { - if( _seriesMetaCache.TryGetValue( key, out info ) ) - { - return info; - } - } + _seriesMetaCache.TryGetValue( key, out info ); } - // get metadata information from the store - var fieldTask = ShowFieldKeysAsync( db, measurementName ); - var tagTask = ShowTagKeysAsync( db, measurementName ); - await Task.WhenAll( fieldTask, tagTask ).ConfigureAwait( false ); - - var fields = fieldTask.Result.Series.FirstOrDefault()?.Rows; - var tags = tagTask.Result.Series.FirstOrDefault()?.Rows; - - info = new DatabaseMeasurementInfo(); - if( fields != null ) + var now = DateTime.UtcNow; + if( info != null ) { - foreach( var row in fields ) + if( !expiration.HasValue ) // info never expires + { + return info; + } + + if( now - info.Timestamp < expiration.Value ) // has not expired { - info.Fields.Add( row.FieldKey ); + return info; } } + + // has expired or never existed, lets retrieve it + + // get metadata information from the store + var tagsResult = await ShowTagKeysAsync( db, measurementName ); + var tags = tagsResult.Series.FirstOrDefault()?.Rows; + + info = new DatabaseMeasurementInfo( now ); if( tags != null ) { foreach( var row in tags ) @@ -895,21 +894,21 @@ private async Task> ExecuteQueryInternalAsync( this, queryResult, db, options.Precision, false ).ConfigureAwait( false ); + return await ResultSetFactory.CreateAsync( this, queryResult, db, options.Precision, true, options.MetadataExpiration ).ConfigureAwait( false ); } private async Task> ExecuteQueryInternalAsync( string query, string db ) where TInfluxRow : new() { var queryResult = await GetInternalAsync( CreateQueryUrl( query, db ), false ).ConfigureAwait( false ); - return await ResultSetFactory.CreateAsync( this, queryResult, db, null, true ).ConfigureAwait( false ); + return await ResultSetFactory.CreateAsync( this, queryResult, db, null, false, null ).ConfigureAwait( false ); } private async Task> ExecuteQueryInternalAsync( string query ) where TInfluxRow : new() { var queryResult = await GetInternalAsync( CreateQueryUrl( query ), false ).ConfigureAwait( false ); - return await ResultSetFactory.CreateAsync( this, queryResult, null, null, false ).ConfigureAwait( false ); + return await ResultSetFactory.CreateAsync( this, queryResult, null, null, false, null ).ConfigureAwait( false ); } private async Task ExecuteQueryInternalAsync( string query, string db ) diff --git a/src/Vibrant.InfluxDB.Client/InfluxComputedAttribute.cs b/src/Vibrant.InfluxDB.Client/InfluxComputedAttribute.cs new file mode 100644 index 0000000..9ce2505 --- /dev/null +++ b/src/Vibrant.InfluxDB.Client/InfluxComputedAttribute.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Vibrant.InfluxDB.Client +{ + /// + /// Attribute to be placed on properties that are considered fields by InfluxDB. + /// + [AttributeUsage( AttributeTargets.Property, Inherited = false, AllowMultiple = false )] + public sealed class InfluxComputedAttribute : InfluxAttribute + { + private readonly string _name; + + /// + /// Constructs an InfluxFieldAttribute with the given name. + /// + /// + public InfluxComputedAttribute( string name ) + { + _name = name; + } + + /// + /// Gets the name of the field used by InfluxDB. + /// + public string Name + { + get { return _name; } + } + } +} diff --git a/src/Vibrant.InfluxDB.Client/InfluxQueryOptions.cs b/src/Vibrant.InfluxDB.Client/InfluxQueryOptions.cs index fa886bf..ebc712b 100644 --- a/src/Vibrant.InfluxDB.Client/InfluxQueryOptions.cs +++ b/src/Vibrant.InfluxDB.Client/InfluxQueryOptions.cs @@ -18,6 +18,7 @@ public InfluxQueryOptions() { Precision = null; ChunkSize = null; + MetadataExpiration = TimeSpan.FromHours( 1 ); } /// @@ -30,5 +31,18 @@ public InfluxQueryOptions() /// it uses the InfluxDB default of 10000. /// public int? ChunkSize { get; set; } + + /// + /// Gets or sets how long before retrieved metadata about measurements + /// takes to expire and must be retrieved again. + /// + /// This is only used when querying data based on the IInfluxRow interface + /// because this interface. This is because the interface has no way to + /// know which retrieved columns are fields or tags. It therefore makes an + /// implicit query to get this information from the database. + /// + /// A value of null means it never expires. Default is 1 hour. + /// + public TimeSpan? MetadataExpiration { get; set; } } } diff --git a/src/Vibrant.InfluxDB.Client/Metadata/DatabaseMeasurementInfo.cs b/src/Vibrant.InfluxDB.Client/Metadata/DatabaseMeasurementInfo.cs index 1f5a618..af9ab55 100644 --- a/src/Vibrant.InfluxDB.Client/Metadata/DatabaseMeasurementInfo.cs +++ b/src/Vibrant.InfluxDB.Client/Metadata/DatabaseMeasurementInfo.cs @@ -20,13 +20,13 @@ public DatabaseMeasurementInfoKey( string db, string measurementName ) internal class DatabaseMeasurementInfo { + internal readonly DateTime Timestamp; internal readonly HashSet Tags; - internal readonly HashSet Fields; - public DatabaseMeasurementInfo() + public DatabaseMeasurementInfo( DateTime timestamp ) { + Timestamp = timestamp; Tags = new HashSet(); - Fields = new HashSet(); } } } diff --git a/src/Vibrant.InfluxDB.Client/Metadata/InfluxRowTypeInfo.cs b/src/Vibrant.InfluxDB.Client/Metadata/InfluxRowTypeInfo.cs index 921192b..10e7859 100644 --- a/src/Vibrant.InfluxDB.Client/Metadata/InfluxRowTypeInfo.cs +++ b/src/Vibrant.InfluxDB.Client/Metadata/InfluxRowTypeInfo.cs @@ -14,6 +14,7 @@ internal class InfluxRowTypeInfo internal readonly PropertyExpressionInfo Timestamp; internal readonly IReadOnlyList> Tags; internal readonly IReadOnlyList> Fields; + internal readonly IReadOnlyList> Computed; internal readonly IReadOnlyDictionary> All; internal readonly IReadOnlyDictionary> PropertiesByClrName; @@ -21,11 +22,13 @@ internal InfluxRowTypeInfo( PropertyExpressionInfo timestamp, List> tags, List> fields, + List> computed, List> all ) { Timestamp = timestamp; Tags = new List>( tags.OrderBy( x => x.Key, StringComparer.Ordinal ) ); Fields = new List>( fields.OrderBy( x => x.Key, StringComparer.Ordinal ) ); + Computed = new List>( computed.OrderBy( x => x.Key, StringComparer.Ordinal ) ); All = new ReadOnlyDictionary>( all.ToDictionary( x => x.Key, x => x ) ); PropertiesByClrName = All.ToDictionary( x => x.Value.Property.Name, x => x.Value ); diff --git a/src/Vibrant.InfluxDB.Client/Metadata/MetadataCache.cs b/src/Vibrant.InfluxDB.Client/Metadata/MetadataCache.cs index d8a05e9..accc0f6 100644 --- a/src/Vibrant.InfluxDB.Client/Metadata/MetadataCache.cs +++ b/src/Vibrant.InfluxDB.Client/Metadata/MetadataCache.cs @@ -23,6 +23,7 @@ internal static InfluxRowTypeInfo GetOrCreate() if ( !_typeCache.TryGetValue( type, out cache ) ) { + var computed = new List>(); var tags = new List>(); var fields = new List>(); var all = new List>(); @@ -31,6 +32,7 @@ internal static InfluxRowTypeInfo GetOrCreate() { var fieldAttribute = propertyInfo.GetCustomAttribute(); var tagAttribute = propertyInfo.GetCustomAttribute(); + var computedAttribute = propertyInfo.GetCustomAttribute(); var timestampAttribute = propertyInfo.GetCustomAttribute(); // list all attributes so we can ensure the attributes specified on a property are valid @@ -85,9 +87,25 @@ internal static InfluxRowTypeInfo GetOrCreate() tags.Add( expression ); all.Add( expression ); } + else if( computedAttribute != null ) + { + var expression = new PropertyExpressionInfo( computedAttribute.Name, propertyInfo ); + if( !_validFieldTypes.Contains( expression.Type ) && !expression.Type.GetTypeInfo().IsEnum ) + { + throw new InfluxException( string.Format( Errors.InvalidComputedType, propertyInfo.Name, type.Name ) ); + } + + if( string.IsNullOrEmpty( computedAttribute.Name ) ) + { + throw new InfluxException( string.Format( Errors.InvalidNameProperty, propertyInfo.Name, type.Name ) ); + } + + computed.Add( expression ); + all.Add( expression ); + } } - cache = new InfluxRowTypeInfo( timestamp, tags, fields, all ); + cache = new InfluxRowTypeInfo( timestamp, tags, fields, computed, all ); _typeCache.Add( typeof( TInfluxRow ), cache ); } diff --git a/src/Vibrant.InfluxDB.Client/Parsers/ResultSetFactory.cs b/src/Vibrant.InfluxDB.Client/Parsers/ResultSetFactory.cs index 6a2de42..d52e88c 100644 --- a/src/Vibrant.InfluxDB.Client/Parsers/ResultSetFactory.cs +++ b/src/Vibrant.InfluxDB.Client/Parsers/ResultSetFactory.cs @@ -22,18 +22,6 @@ internal static bool IsIInfluxRow() return typeof( IInfluxRow ).IsAssignableFrom( typeof( TInfluxRow ) ); } - private static bool HasAllColumns( DatabaseMeasurementInfo meta, List columns ) - { - foreach( var fieldOrTag in columns ) - { - if( fieldOrTag != InfluxConstants.KeyColumn && fieldOrTag != InfluxConstants.TimeColumn && !meta.Tags.Contains( fieldOrTag ) && !meta.Fields.Contains( fieldOrTag ) ) - { - return false; - } - } - return true; - } - internal static InfluxResultSet Create( QueryResult queryResult ) { List results = new List(); @@ -70,12 +58,13 @@ internal static Task> CreateAsync( IEnumerable queryResult, string db, TimestampPrecision? precision, - bool isExclusivelyFields ) + bool allowMetadataQuerying, + TimeSpan? metadataExpiration ) where TInfluxRow : new() { if( IsIInfluxRow() ) { - return CreateBasedOnInterfaceAsync( client, queryResult, db, precision, isExclusivelyFields ); + return CreateBasedOnInterfaceAsync( client, queryResult, db, precision, allowMetadataQuerying, metadataExpiration ); } else { @@ -246,7 +235,8 @@ private async static Task> CreateBasedOnInterfaceAsy IEnumerable queryResults, string db, TimestampPrecision? precision, - bool isExclusivelyFields ) + bool allowMetadataQuerying, + TimeSpan? metadataExpiration ) where TInfluxRow : new() { // In this case, we will contruct objects based on the IInfluxRow interface @@ -289,33 +279,19 @@ private async static Task> CreateBasedOnInterfaceAsy { // Get metadata information about the measurement we are querying, as we dont know // which columns are tags/fields otherwise + + // PROBLEM: Should NOT always be called! DatabaseMeasurementInfo meta = null; - if( !isExclusivelyFields ) + if( allowMetadataQuerying ) { - // get the required metadata - meta = await client.GetMetaInformationAsync( db, name, false ).ConfigureAwait( false ); - - // check that we have all columns, otherwise call method again - bool hasAllColumnsAndTags = HasAllColumns( meta, columns ); - if( !hasAllColumnsAndTags ) - { - // if we dont have all columns, attempt to query the metadata again (might have changed since last query) - meta = await client.GetMetaInformationAsync( db, name, false ).ConfigureAwait( false ); - hasAllColumnsAndTags = HasAllColumns( meta, columns ); - - // if we still dont have all columns, we cant do anything, throw exception - if( !hasAllColumnsAndTags ) - { - throw new InfluxException( Errors.IndeterminateColumns ); - } - } + meta = await client.GetMetaInformationAsync( db, name, metadataExpiration ).ConfigureAwait( false ); ; } for( int i = 0 ; i < columns.Count ; i++ ) { var columnName = columns[ i ]; - if( isExclusivelyFields ) + if( !allowMetadataQuerying ) { setters[ i ] = ( row, fieldName, value ) => row.SetField( fieldName, value ); } @@ -337,13 +313,9 @@ private async static Task> CreateBasedOnInterfaceAsy { setters[ i ] = ( row, tagName, value ) => row.SetTag( tagName, (string)value ); } - else if( meta.Fields.Contains( columnName ) ) - { - setters[ i ] = ( row, fieldName, value ) => row.SetField( fieldName, value ); - } else { - throw new InfluxException( string.Format( Errors.InvalidColumn, columnName ) ); + setters[ i ] = ( row, fieldName, value ) => row.SetField( fieldName, value ); } } diff --git a/src/Vibrant.InfluxDB.Client/Resources/Errors.cs b/src/Vibrant.InfluxDB.Client/Resources/Errors.cs index b055f2e..a2957e6 100644 --- a/src/Vibrant.InfluxDB.Client/Resources/Errors.cs +++ b/src/Vibrant.InfluxDB.Client/Resources/Errors.cs @@ -12,7 +12,8 @@ internal static class Errors internal static readonly string CountNotConvertEnumToString = "Could not convert the incominng value {0} to the enum on the property {1} on the type {2}."; internal static readonly string IndeterminateColumns = "Could not determine which columns in the returned data are tags and which are fields."; internal static readonly string InvalidFieldType = "The property {0} on the type {1} which is used as an InfluxField must be one of the following types: string, double, long, bool, DateTime, Nullable, Nullable, Nullable, Nullable or a user-defined enum."; - internal static readonly string InvalidNameProperty = "The property {0} on the type {1} must specify a non-empty name for either an InfluxField or InfluxTag."; + internal static readonly string InvalidComputedType = "The property {0} on the type {1} which is used as an InfluxComputed must be one of the following types: string, double, long, bool, DateTime, Nullable, Nullable, Nullable, Nullable or a user-defined enum."; + internal static readonly string InvalidNameProperty = "The property {0} on the type {1} must specify a non-empty name for either an InfluxField, InfluxTag or InfluxComputed."; internal static readonly string InvalidTagType = "The property {0} on the type {1} which is used as an InfluxTag must be either a string or a user-defined enum."; internal static readonly string InvalidTimestampType = "The property {0} on the type {1} which is used as the InfluxTimestamp must be either a DateTime or a Nullable."; internal static readonly string MultipleAttributesOnSingleProperty = "The property {0} on the type {1} has multiple InfluxAttributes. This is not allowed. Please specify only InfluxTimestamp, InfluxTag or InfluxField."; diff --git a/src/Vibrant.InfluxDB.Client/Vibrant.InfluxDB.Client.csproj b/src/Vibrant.InfluxDB.Client/Vibrant.InfluxDB.Client.csproj index 8e464c6..4d668d2 100644 --- a/src/Vibrant.InfluxDB.Client/Vibrant.InfluxDB.Client.csproj +++ b/src/Vibrant.InfluxDB.Client/Vibrant.InfluxDB.Client.csproj @@ -4,7 +4,6 @@ An easy-to-use client for InfluxDB that supports simple query to object mapping. Copyright (c) 2015-2016 MikaelGRA InfluxDB Client for .NET - 3.0.3 MikaelGRA net45;netstandard1.3 Vibrant.InfluxDB.Client @@ -21,7 +20,7 @@ false true True - 3.0.4 + 3.1.0 diff --git a/test/Vibrant.InfluxDB.Client.Tests/ComputerInfo.cs b/test/Vibrant.InfluxDB.Client.Tests/ComputerInfo.cs index d779808..67a2888 100644 --- a/test/Vibrant.InfluxDB.Client.Tests/ComputerInfo.cs +++ b/test/Vibrant.InfluxDB.Client.Tests/ComputerInfo.cs @@ -23,6 +23,17 @@ public class ComputerInfo [InfluxField( "ram" )] internal long RAM { get; set; } } + public class ComputedComputerInfo + { + [InfluxTimestamp] + internal DateTime Timestamp { get; set; } + + [InfluxComputed( "cpu" )] + internal double? CPU { get; set; } + + [InfluxComputed( "ram" )] + internal long RAM { get; set; } + } public class ComputerInfoMeta { diff --git a/test/Vibrant.InfluxDB.Client.Tests/InfluxClientFixture.cs b/test/Vibrant.InfluxDB.Client.Tests/InfluxClientFixture.cs index a11648e..4d8df2a 100644 --- a/test/Vibrant.InfluxDB.Client.Tests/InfluxClientFixture.cs +++ b/test/Vibrant.InfluxDB.Client.Tests/InfluxClientFixture.cs @@ -7,136 +7,136 @@ namespace Vibrant.InfluxDB.Client.Tests { - public sealed class InfluxClientFixture : IDisposable - { - private bool _disposed; - - public const string DatabaseName = "unittestdb"; - //public static readonly string InfluxHost = "http://winflux.westeurope.cloudapp.azure.com:8086"; - //public static readonly string InfluxHost = "http://localhost:8086"; http://ipv4.fiddler - public static readonly string InfluxHost = "http://ipv4.fiddler:8086"; - //public static readonly string InfluxHost = "http://52.174.58.40:8086"; - - public InfluxClient Client { get; set; } - - public InfluxClientFixture() - { - Client = new InfluxClient(new Uri(InfluxHost), "root", "root"); - Client.CreateDatabaseAsync(DatabaseName).Wait(); - } - - public static readonly string[] Regions = new[] { "west-eu", "north-eu", "west-us", "east-us", "asia" }; - public static readonly string[] Hosts = new[] { "ma-lt", "surface-book" }; - public static readonly TestEnum1?[] TestEnums = new TestEnum1?[] { null, TestEnum1.Value1, TestEnum1.Value2, TestEnum1.Value3 }; - - public static ComputerInfo[] CreateTypedRowsStartingAt(DateTime start, int rows, bool includeNulls) - { - var rng = new Random(); - - var timestamp = start; - var infos = new ComputerInfo[rows]; - for (int i = 0; i < rows; i++) + public sealed class InfluxClientFixture : IDisposable + { + private bool _disposed; + + public const string DatabaseName = "unittestdb"; + //public static readonly string InfluxHost = "http://winflux.westeurope.cloudapp.azure.com:8086"; + //public static readonly string InfluxHost = "http://localhost:8086"; http://ipv4.fiddler + public static readonly string InfluxHost = "http://ipv4.fiddler:8086"; + //public static readonly string InfluxHost = "http://52.174.58.40:8086"; + + public InfluxClient Client { get; set; } + + public InfluxClientFixture() + { + Client = new InfluxClient( new Uri( InfluxHost ), "root", "root" ); + Client.CreateDatabaseAsync( DatabaseName ).Wait(); + } + + public static readonly string[] Regions = new[] { "west-eu", "north-eu", "west-us", "east-us", "asia" }; + public static readonly string[] Hosts = new[] { "ma-lt", "surface-book" }; + public static readonly TestEnum1?[] TestEnums = new TestEnum1?[] { null, TestEnum1.Value1, TestEnum1.Value2, TestEnum1.Value3 }; + + public static ComputerInfo[] CreateTypedRowsStartingAt( DateTime start, int rows, bool includeNulls ) + { + var rng = new Random(); + + var timestamp = start; + var infos = new ComputerInfo[ rows ]; + for( int i = 0 ; i < rows ; i++ ) + { + long ram = rng.Next( int.MaxValue ); + double cpu = rng.NextDouble(); + string region = Regions[ rng.Next( Regions.Length ) ]; + string host = Hosts[ rng.Next( Hosts.Length ) ]; + + if( includeNulls ) { - long ram = rng.Next(int.MaxValue); - double cpu = rng.NextDouble(); - string region = Regions[rng.Next(Regions.Length)]; - string host = Hosts[rng.Next(Hosts.Length)]; - - if (includeNulls) - { - var info = new ComputerInfo { Timestamp = timestamp, RAM = ram, Region = region }; - infos[i] = info; - } - else - { - var info = new ComputerInfo { Timestamp = timestamp, CPU = cpu, RAM = ram, Host = host, Region = region }; - infos[i] = info; - } - - timestamp = timestamp.AddSeconds(1); + var info = new ComputerInfo { Timestamp = timestamp, RAM = ram, Region = region }; + infos[ i ] = info; } - - return infos; - } - - public static EnumeratedRow[] CreateEnumeratedRowsStartingAt(DateTime start, int rows) - { - var rng = new Random(); - - var timestamp = start; - var infos = new EnumeratedRow[rows]; - for (int i = 0; i < rows; i++) - { - var value = rng.NextDouble(); - var type = TestEnums[rng.Next(TestEnums.Length)]; - - var info = new EnumeratedRow { Timestamp = timestamp, Value = value, Type = type }; - infos[i] = info; - - timestamp = timestamp.AddSeconds(1); - } - - return infos; - } - - public static DynamicInfluxRow[] CreateDynamicRowsStartingAt(DateTime start, int rows) - { - var rng = new Random(); - - var timestamp = start; - var infos = new DynamicInfluxRow[rows]; - for (int i = 0; i < rows; i++) - { - long ram = rng.Next(int.MaxValue); - double cpu = rng.NextDouble(); - string region = Regions[rng.Next(Regions.Length)]; - string host = Hosts[rng.Next(Hosts.Length)]; - - var info = new DynamicInfluxRow(); - info.Fields.Add("cpu", cpu); - info.Fields.Add("ram", ram); - info.Tags.Add("host", host); - info.Tags.Add("region", region); - info.Timestamp = timestamp; - - infos[i] = info; - - timestamp = timestamp.AddSeconds(1); - } - return infos; - } - - #region IDisposable - - /// - /// Destructor. - /// - ~InfluxClientFixture() - { - Dispose(false); - } - - /// - /// Disposes the InfluxClient and the internal HttpClient that it uses. - /// - public void Dispose() - { - if (!_disposed) - { - Dispose(true); - _disposed = true; - GC.SuppressFinalize(this); - } - } - - private void Dispose(bool disposing) - { - if (disposing) + else { - Client.DropDatabaseAsync(DatabaseName).Wait(); + var info = new ComputerInfo { Timestamp = timestamp, CPU = cpu, RAM = ram, Host = host, Region = region }; + infos[ i ] = info; } - } - #endregion - } + timestamp = timestamp.AddSeconds( 1 ); + } + + return infos; + } + + public static EnumeratedRow[] CreateEnumeratedRowsStartingAt( DateTime start, int rows ) + { + var rng = new Random(); + + var timestamp = start; + var infos = new EnumeratedRow[ rows ]; + for( int i = 0 ; i < rows ; i++ ) + { + var value = rng.NextDouble(); + var type = TestEnums[ rng.Next( TestEnums.Length ) ]; + + var info = new EnumeratedRow { Timestamp = timestamp, Value = value, Type = type }; + infos[ i ] = info; + + timestamp = timestamp.AddSeconds( 1 ); + } + + return infos; + } + + public static DynamicInfluxRow[] CreateDynamicRowsStartingAt( DateTime start, int rows ) + { + var rng = new Random(); + + var timestamp = start; + var infos = new DynamicInfluxRow[ rows ]; + for( int i = 0 ; i < rows ; i++ ) + { + long ram = rng.Next( int.MaxValue ); + double cpu = rng.NextDouble(); + string region = Regions[ rng.Next( Regions.Length ) ]; + string host = Hosts[ rng.Next( Hosts.Length ) ]; + + var info = new DynamicInfluxRow(); + info.Fields.Add( "cpu", cpu ); + info.Fields.Add( "ram", ram ); + info.Tags.Add( "host", host ); + info.Tags.Add( "region", region ); + info.Timestamp = timestamp; + + infos[ i ] = info; + + timestamp = timestamp.AddSeconds( 1 ); + } + return infos; + } + + #region IDisposable + + /// + /// Destructor. + /// + ~InfluxClientFixture() + { + Dispose( false ); + } + + /// + /// Disposes the InfluxClient and the internal HttpClient that it uses. + /// + public void Dispose() + { + if( !_disposed ) + { + Dispose( true ); + _disposed = true; + GC.SuppressFinalize( this ); + } + } + + private void Dispose( bool disposing ) + { + if( disposing ) + { + Client.DropDatabaseAsync( DatabaseName ).Wait(); + } + } + + #endregion + } } diff --git a/test/Vibrant.InfluxDB.Client.Tests/ReadWriteTests.cs b/test/Vibrant.InfluxDB.Client.Tests/ReadWriteTests.cs index da492ca..05f6489 100644 --- a/test/Vibrant.InfluxDB.Client.Tests/ReadWriteTests.cs +++ b/test/Vibrant.InfluxDB.Client.Tests/ReadWriteTests.cs @@ -8,247 +8,267 @@ namespace Vibrant.InfluxDB.Client.Tests { - [Collection("InfluxClient collection")] - public class ReadWriteTests - { - private const string Unused = "unuseddatabasename"; - - private InfluxClient _client; - - public ReadWriteTests(InfluxClientFixture fixture) - { - _client = fixture.Client; - } - - [Fact] - public async Task Should_Write_Typed_Rows_To_Database_With_Chunking() - { - var infos = InfluxClientFixture.CreateTypedRowsStartingAt(new DateTime(2010, 1, 1, 1, 1, 1, DateTimeKind.Utc), 20000, false); - await _client.WriteAsync(InfluxClientFixture.DatabaseName, "computerInfo1", infos); - - var resultSet = await _client.ReadAsync( - InfluxClientFixture.DatabaseName, - "select * from computerInfo1", - new InfluxQueryOptions { ChunkSize = 1000 }); - - var result = resultSet.Results[0]; - Assert.Equal(1, result.Series.Count); - - var series = result.Series[0]; - Assert.Equal(20000, series.Rows.Count); - } - - [Theory] - [InlineData(500, 2011, "computerInfo2_500")] - [InlineData(1000, 2012, "computerInfo2_1000")] - [InlineData(20000, 2013, "computerInfo2_20000")] - public async Task Should_Write_Typed_Rows_To_Database(int rows, int startYear, string tableName) - { - var infos = InfluxClientFixture.CreateTypedRowsStartingAt(new DateTime(startYear, 1, 1, 1, 1, 1, DateTimeKind.Utc), rows, false); - await _client.WriteAsync(InfluxClientFixture.DatabaseName, tableName, infos); - - var secondResultSet = await _client.ReadAsync(InfluxClientFixture.DatabaseName, $"select * from {tableName}"); - - var result = secondResultSet.Results[0]; - Assert.Equal(1, result.Series.Count); - - var series = result.Series[0]; - Assert.Equal(rows, series.Rows.Count); - } - - [Theory] - [InlineData(500, 2011)] - [InlineData(1000, 2012)] - [InlineData(20000, 2013)] - public async Task Should_Write_Typed_Rows_With_Nulls_To_Database(int rows, int startYear) - { - var infos = InfluxClientFixture.CreateTypedRowsStartingAt(new DateTime(startYear, 1, 1, 1, 1, 1, DateTimeKind.Utc), rows, true); - await _client.WriteAsync(InfluxClientFixture.DatabaseName, "computerInfo3", infos); - } - - [Theory] - [InlineData(500, 2011)] - [InlineData(1000, 2012)] - [InlineData(20000, 2013)] - public async Task Should_Write_Dynamic_Rows_To_Database(int rows, int startYear) - { - var infos = InfluxClientFixture.CreateDynamicRowsStartingAt(new DateTime(startYear, 1, 1, 1, 1, 1, DateTimeKind.Utc), rows); - await _client.WriteAsync(InfluxClientFixture.DatabaseName, "computerInfo4", infos); - } - - [Fact] - public async Task Should_Write_All_Field_Types_To_Database() - { - var row = new VariationRow - { - Timestamp = new DateTime(2013, 1, 1, 1, 1, 1, DateTimeKind.Utc), - Count = 1337, - Indicator = true, - Message = "Hello there\nWhat's up?", - Percent = 0.37, - Type = "tag Value", - Category = TestEnum1.Value2, - CategoryTag = TestEnum2.Value3, - OtherTimestamp = new DateTime(2011, 4, 23, 1, 23, 54, DateTimeKind.Utc), - }; - - await _client.WriteAsync(InfluxClientFixture.DatabaseName, "variation", new[] { row }); - - var resultSet = await _client.ReadAsync(InfluxClientFixture.DatabaseName, "SELECT * FROM variation"); - Assert.Equal(1, resultSet.Results.Count); - - var result = resultSet.Results[0]; - Assert.Equal(1, result.Series.Count); - - var series = result.Series[0]; - Assert.Equal(1, series.Rows.Count); - - Assert.Equal(row, series.Rows[0]); - } - - [Fact] - public async Task Should_Write_And_Query_Typed_Data() - { - var start = new DateTime(2013, 1, 1, 1, 1, 1, DateTimeKind.Utc); - var infos = InfluxClientFixture.CreateTypedRowsStartingAt(start, 500, false); - await _client.WriteAsync(InfluxClientFixture.DatabaseName, "computerInfo5", infos); - + [Collection( "InfluxClient collection" )] + public class ReadWriteTests + { + private const string Unused = "unuseddatabasename"; + + private InfluxClient _client; + + public ReadWriteTests( InfluxClientFixture fixture ) + { + _client = fixture.Client; + } + + [Fact] + public async Task Should_Write_Typed_Rows_To_Database_With_Chunking() + { + var infos = InfluxClientFixture.CreateTypedRowsStartingAt( new DateTime( 2010, 1, 1, 1, 1, 1, DateTimeKind.Utc ), 20000, false ); + await _client.WriteAsync( InfluxClientFixture.DatabaseName, "computerInfo1", infos ); + + var resultSet = await _client.ReadAsync( + InfluxClientFixture.DatabaseName, + "select * from computerInfo1", + new InfluxQueryOptions { ChunkSize = 1000 } ); + + var result = resultSet.Results[ 0 ]; + Assert.Equal( 1, result.Series.Count ); + + var series = result.Series[ 0 ]; + Assert.Equal( 20000, series.Rows.Count ); + } + + [Theory] + [InlineData( 500, 2011, "computerInfo2_500" )] + [InlineData( 1000, 2012, "computerInfo2_1000" )] + [InlineData( 20000, 2013, "computerInfo2_20000" )] + public async Task Should_Write_Typed_Rows_To_Database( int rows, int startYear, string tableName ) + { + var infos = InfluxClientFixture.CreateTypedRowsStartingAt( new DateTime( startYear, 1, 1, 1, 1, 1, DateTimeKind.Utc ), rows, false ); + await _client.WriteAsync( InfluxClientFixture.DatabaseName, tableName, infos ); + + var secondResultSet = await _client.ReadAsync( InfluxClientFixture.DatabaseName, $"select * from {tableName}" ); + + var result = secondResultSet.Results[ 0 ]; + Assert.Equal( 1, result.Series.Count ); + + var series = result.Series[ 0 ]; + Assert.Equal( rows, series.Rows.Count ); + } + + [Theory] + [InlineData( 500, 2011 )] + [InlineData( 1000, 2012 )] + [InlineData( 20000, 2013 )] + public async Task Should_Write_Typed_Rows_With_Nulls_To_Database( int rows, int startYear ) + { + var infos = InfluxClientFixture.CreateTypedRowsStartingAt( new DateTime( startYear, 1, 1, 1, 1, 1, DateTimeKind.Utc ), rows, true ); + await _client.WriteAsync( InfluxClientFixture.DatabaseName, "computerInfo3", infos ); + } + + [Theory] + [InlineData( 500, 2011 )] + [InlineData( 1000, 2012 )] + [InlineData( 20000, 2013 )] + public async Task Should_Write_Dynamic_Rows_To_Database( int rows, int startYear ) + { + var infos = InfluxClientFixture.CreateDynamicRowsStartingAt( new DateTime( startYear, 1, 1, 1, 1, 1, DateTimeKind.Utc ), rows ); + await _client.WriteAsync( InfluxClientFixture.DatabaseName, "computerInfo4", infos ); + } + + [Fact] + public async Task Should_Write_All_Field_Types_To_Database() + { + var row = new VariationRow + { + Timestamp = new DateTime( 2013, 1, 1, 1, 1, 1, DateTimeKind.Utc ), + Count = 1337, + Indicator = true, + Message = "Hello there\nWhat's up?", + Percent = 0.37, + Type = "tag Value", + Category = TestEnum1.Value2, + CategoryTag = TestEnum2.Value3, + OtherTimestamp = new DateTime( 2011, 4, 23, 1, 23, 54, DateTimeKind.Utc ), + }; + + await _client.WriteAsync( InfluxClientFixture.DatabaseName, "variation", new[] { row } ); + + var resultSet = await _client.ReadAsync( InfluxClientFixture.DatabaseName, "SELECT * FROM variation" ); + Assert.Equal( 1, resultSet.Results.Count ); + + var result = resultSet.Results[ 0 ]; + Assert.Equal( 1, result.Series.Count ); + + var series = result.Series[ 0 ]; + Assert.Equal( 1, series.Rows.Count ); + + Assert.Equal( row, series.Rows[ 0 ] ); + } + + [Fact] + public async Task Should_Write_And_Query_Typed_Data() + { + var start = new DateTime( 2013, 1, 1, 1, 1, 1, DateTimeKind.Utc ); + var infos = InfluxClientFixture.CreateTypedRowsStartingAt( start, 500, false ); + await _client.WriteAsync( InfluxClientFixture.DatabaseName, "computerInfo5", infos ); + + + var from = start; + var to = from.AddSeconds( 250 ); + + var resultSet = await _client.ReadAsync( InfluxClientFixture.DatabaseName, $"SELECT * FROM computerInfo5 WHERE '{from.ToIso8601()}' <= time AND time < '{to.ToIso8601()}'" ); + Assert.Equal( 1, resultSet.Results.Count ); + + var result = resultSet.Results[ 0 ]; + Assert.Equal( 1, result.Series.Count ); + + var series = result.Series[ 0 ]; + Assert.Equal( 250, series.Rows.Count ); + + // attempt deletion + await _client.DeleteRangeAsync( InfluxClientFixture.DatabaseName, "computerInfo5", from, to ); + + resultSet = await _client.ReadAsync( InfluxClientFixture.DatabaseName, $"SELECT * FROM computerInfo5 WHERE '{from.ToIso8601()}' <= time AND time < '{to.ToIso8601()}'" ); + Assert.Equal( 1, resultSet.Results.Count ); + + result = resultSet.Results[ 0 ]; + Assert.Equal( 0, result.Series.Count ); + } + + [Fact] + public async Task Should_Write_Read_And_Delete_Typed_Data() + { + for( int i = 0 ; i < 2 ; i++ ) + { + var start = new DateTime( 2011, 1, 1, 1, 1, 1, DateTimeKind.Utc ); + var infos = InfluxClientFixture.CreateTypedRowsStartingAt( start, 250, false ); + await _client.WriteAsync( InfluxClientFixture.DatabaseName, "otherData", infos ); var from = start; - var to = from.AddSeconds(250); + var to = from.AddSeconds( 250 ); - var resultSet = await _client.ReadAsync(InfluxClientFixture.DatabaseName, $"SELECT * FROM computerInfo5 WHERE '{from.ToIso8601()}' <= time AND time < '{to.ToIso8601()}'"); - Assert.Equal(1, resultSet.Results.Count); + var resultSet = await _client.ReadAsync( InfluxClientFixture.DatabaseName, $"SELECT * FROM otherData WHERE '{from.ToIso8601()}' <= time AND time < '{to.ToIso8601()}'" ); + Assert.Equal( 1, resultSet.Results.Count ); - var result = resultSet.Results[0]; - Assert.Equal(1, result.Series.Count); + var result = resultSet.Results[ 0 ]; + Assert.Equal( 1, result.Series.Count ); - var series = result.Series[0]; - Assert.Equal(250, series.Rows.Count); + var series = result.Series[ 0 ]; + Assert.Equal( 250, series.Rows.Count ); // attempt deletion - await _client.DeleteRangeAsync(InfluxClientFixture.DatabaseName, "computerInfo5", from, to); - - resultSet = await _client.ReadAsync(InfluxClientFixture.DatabaseName, $"SELECT * FROM computerInfo5 WHERE '{from.ToIso8601()}' <= time AND time < '{to.ToIso8601()}'"); - Assert.Equal(1, resultSet.Results.Count); - - result = resultSet.Results[0]; - Assert.Equal(0, result.Series.Count); - } - - [Fact] - public async Task Should_Write_Read_And_Delete_Typed_Data() - { - for (int i = 0; i < 2; i++) - { - var start = new DateTime(2011, 1, 1, 1, 1, 1, DateTimeKind.Utc); - var infos = InfluxClientFixture.CreateTypedRowsStartingAt(start, 250, false); - await _client.WriteAsync(InfluxClientFixture.DatabaseName, "otherData", infos); - - var from = start; - var to = from.AddSeconds(250); - - var resultSet = await _client.ReadAsync(InfluxClientFixture.DatabaseName, $"SELECT * FROM otherData WHERE '{from.ToIso8601()}' <= time AND time < '{to.ToIso8601()}'"); - Assert.Equal(1, resultSet.Results.Count); - - var result = resultSet.Results[0]; - Assert.Equal(1, result.Series.Count); - - var series = result.Series[0]; - Assert.Equal(250, series.Rows.Count); - - // attempt deletion - await _client.DeleteRangeAsync(InfluxClientFixture.DatabaseName, "otherData", from, to); - - resultSet = await _client.ReadAsync(InfluxClientFixture.DatabaseName, $"SELECT * FROM otherData WHERE '{from.ToIso8601()}' <= time AND time < '{to.ToIso8601()}'"); - Assert.Equal(1, resultSet.Results.Count); - - result = resultSet.Results[0]; - Assert.Equal(0, result.Series.Count); - } - } - - [Fact] - public async Task Should_Write_Type_With_Null_Timestamp() - { - var row = new SimpleRow - { - Value = 13.37 - }; - - await _client.WriteAsync(InfluxClientFixture.DatabaseName, "simpleRow", new[] { row }); - - var resultSet = await _client.ReadAsync(InfluxClientFixture.DatabaseName, "SELECT * FROM simpleRow"); - Assert.Equal(1, resultSet.Results.Count); - - var result = resultSet.Results[0]; - Assert.Equal(1, result.Series.Count); - - var series = result.Series[0]; - Assert.Equal(1, series.Rows.Count); - - Assert.Equal(row.Value, series.Rows[0].Value); - } - - [Fact] - public async Task Should_Write_And_Query_Grouped_Data() - { - var start = new DateTime(2011, 1, 1, 1, 1, 1, DateTimeKind.Utc); - var infos = InfluxClientFixture.CreateTypedRowsStartingAt(start, 5000, false); - await _client.WriteAsync(InfluxClientFixture.DatabaseName, "groupedComputerInfo1", infos); - - var resultSet = await _client.ReadAsync(InfluxClientFixture.DatabaseName, $"SELECT * FROM groupedComputerInfo1 GROUP BY region"); - Assert.Equal(1, resultSet.Results.Count); - - var result = resultSet.Results[0]; - foreach (var region in InfluxClientFixture.Regions) - { - var kvp = new KeyValuePair("region", region); - var group = result.FindGroup("groupedComputerInfo1", new[] { kvp }); - Assert.NotNull(group); - } - } - - [Fact] - public async Task Should_Write_And_Query_Grouped_Data_With_Chunking() - { - var start = new DateTime(2011, 1, 1, 1, 1, 1, DateTimeKind.Utc); - var infos = InfluxClientFixture.CreateTypedRowsStartingAt(start, 5000, false); - await _client.WriteAsync(InfluxClientFixture.DatabaseName, "groupedComputerInfo2", infos); - - var resultSet = await _client.ReadAsync( - InfluxClientFixture.DatabaseName, - $"SELECT * FROM groupedComputerInfo2 GROUP BY region", - new InfluxQueryOptions { ChunkSize = 100 }); - - Assert.Equal(1, resultSet.Results.Count); - - var result = resultSet.Results[0]; - foreach (var region in InfluxClientFixture.Regions) - { - var kvp = new KeyValuePair("region", region); - var group = result.FindGroup("groupedComputerInfo2", new[] { kvp }); - Assert.NotNull(group); - } - } - - [Fact] - public async Task Should_Write_And_Query_Grouped_On_Enumerated_Data() - { - var start = new DateTime(2011, 1, 1, 1, 1, 1, DateTimeKind.Utc); - var infos = InfluxClientFixture.CreateEnumeratedRowsStartingAt(start, 5000); - await _client.WriteAsync(InfluxClientFixture.DatabaseName, "groupedEnumeratedRows", infos); - - var resultSet = await _client.ReadAsync(InfluxClientFixture.DatabaseName, $"SELECT * FROM groupedEnumeratedRows GROUP BY type"); - Assert.Equal(1, resultSet.Results.Count); - - var result = resultSet.Results[0]; - foreach (var type in InfluxClientFixture.TestEnums) - { - var kvp = new KeyValuePair("type", type); - var group = result.FindGroup("groupedEnumeratedRows", new[] { kvp }); - Assert.NotNull(group); - } - } - } + await _client.DeleteRangeAsync( InfluxClientFixture.DatabaseName, "otherData", from, to ); + + resultSet = await _client.ReadAsync( InfluxClientFixture.DatabaseName, $"SELECT * FROM otherData WHERE '{from.ToIso8601()}' <= time AND time < '{to.ToIso8601()}'" ); + Assert.Equal( 1, resultSet.Results.Count ); + + result = resultSet.Results[ 0 ]; + Assert.Equal( 0, result.Series.Count ); + } + } + + [Fact] + public async Task Should_Write_Type_With_Null_Timestamp() + { + var row = new SimpleRow + { + Value = 13.37 + }; + + await _client.WriteAsync( InfluxClientFixture.DatabaseName, "simpleRow", new[] { row } ); + + var resultSet = await _client.ReadAsync( InfluxClientFixture.DatabaseName, "SELECT * FROM simpleRow" ); + Assert.Equal( 1, resultSet.Results.Count ); + + var result = resultSet.Results[ 0 ]; + Assert.Equal( 1, result.Series.Count ); + + var series = result.Series[ 0 ]; + Assert.Equal( 1, series.Rows.Count ); + + Assert.Equal( row.Value, series.Rows[ 0 ].Value ); + } + + [Fact] + public async Task Should_Write_And_Query_Grouped_Data() + { + var start = new DateTime( 2011, 1, 1, 1, 1, 1, DateTimeKind.Utc ); + var infos = InfluxClientFixture.CreateTypedRowsStartingAt( start, 5000, false ); + await _client.WriteAsync( InfluxClientFixture.DatabaseName, "groupedComputerInfo1", infos ); + + var resultSet = await _client.ReadAsync( InfluxClientFixture.DatabaseName, $"SELECT * FROM groupedComputerInfo1 GROUP BY region" ); + Assert.Equal( 1, resultSet.Results.Count ); + + var result = resultSet.Results[ 0 ]; + foreach( var region in InfluxClientFixture.Regions ) + { + var kvp = new KeyValuePair( "region", region ); + var group = result.FindGroup( "groupedComputerInfo1", new[] { kvp } ); + Assert.NotNull( group ); + } + } + + [Fact] + public async Task Should_Write_And_Query_Grouped_Data_With_Chunking() + { + var start = new DateTime( 2011, 1, 1, 1, 1, 1, DateTimeKind.Utc ); + var infos = InfluxClientFixture.CreateTypedRowsStartingAt( start, 5000, false ); + await _client.WriteAsync( InfluxClientFixture.DatabaseName, "groupedComputerInfo2", infos ); + + var resultSet = await _client.ReadAsync( + InfluxClientFixture.DatabaseName, + $"SELECT * FROM groupedComputerInfo2 GROUP BY region", + new InfluxQueryOptions { ChunkSize = 100 } ); + + Assert.Equal( 1, resultSet.Results.Count ); + + var result = resultSet.Results[ 0 ]; + foreach( var region in InfluxClientFixture.Regions ) + { + var kvp = new KeyValuePair( "region", region ); + var group = result.FindGroup( "groupedComputerInfo2", new[] { kvp } ); + Assert.NotNull( group ); + } + } + + [Fact] + public async Task Should_Write_And_Query_Grouped_Data_Using_Computed_Columns() + { + var start = new DateTime( 2011, 1, 1, 1, 1, 1, DateTimeKind.Utc ); + var infos = InfluxClientFixture.CreateTypedRowsStartingAt( start, 5000, false ); + var end = infos.Last().Timestamp; + await _client.WriteAsync( InfluxClientFixture.DatabaseName, "groupedComputerInfo3", infos ); + + var resultSet = await _client.ReadAsync( InfluxClientFixture.DatabaseName, $"SELECT MEAN(cpu) AS cpu, COUNT(ram) AS ram FROM groupedComputerInfo3 WHERE time >= '{start.ToIso8601()}' AND time <= '{end.ToIso8601()}' GROUP BY time(1ms), region fill(none)" ); + Assert.Equal( 1, resultSet.Results.Count ); + + var result = resultSet.Results[ 0 ]; + foreach( var region in InfluxClientFixture.Regions ) + { + var kvp = new KeyValuePair( "region", region ); + var group = result.FindGroup( "groupedComputerInfo3", new[] { kvp } ); + Assert.NotNull( group ); + } + } + + [Fact] + public async Task Should_Write_And_Query_Grouped_On_Enumerated_Data() + { + var start = new DateTime( 2011, 1, 1, 1, 1, 1, DateTimeKind.Utc ); + var infos = InfluxClientFixture.CreateEnumeratedRowsStartingAt( start, 5000 ); + await _client.WriteAsync( InfluxClientFixture.DatabaseName, "groupedEnumeratedRows", infos ); + + var resultSet = await _client.ReadAsync( InfluxClientFixture.DatabaseName, $"SELECT * FROM groupedEnumeratedRows GROUP BY type" ); + Assert.Equal( 1, resultSet.Results.Count ); + + var result = resultSet.Results[ 0 ]; + foreach( var type in InfluxClientFixture.TestEnums ) + { + var kvp = new KeyValuePair( "type", type ); + var group = result.FindGroup( "groupedEnumeratedRows", new[] { kvp } ); + Assert.NotNull( group ); + } + } + } }