< Summary - pva.SuperV

Information
Class: pva.SuperV.Engine.HistoryStorage.TDengineHistoryStorage
Assembly: pva.SuperV.Engine
File(s): /home/runner/work/pva.SuperV/pva.SuperV/pva.SuperV.Engine/HistoryStorage/TDengineHistoryStorage.cs
Tag: dotnet-ubuntu_18869653307
Line coverage
86%
Covered lines: 183
Uncovered lines: 29
Coverable lines: 212
Total lines: 372
Line coverage: 86.3%
Branch coverage
82%
Covered branches: 41
Total branches: 50
Branch coverage: 82%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)100%11100%
Connect()100%1166.66%
UpsertRepository(...)50%2270%
DeleteRepository(...)0%620%
UpsertClassTimeSerie(...)75%8885%
HistorizeValues(...)50%2290%
ConvertFieldValueToDb(...)91.66%252488.88%
GetHistoryValues(...)100%2286.95%
GetHistoryStatistics(...)100%4489.65%
FormatToSqlDate(...)100%11100%
FormatInterval(...)100%11100%
GetIntervalPeriod(...)100%22100%
Dispose()100%11100%
Dispose(...)50%22100%
GetFieldDbType(...)100%22100%

File(s)

/home/runner/work/pva.SuperV/pva.SuperV/pva.SuperV.Engine/HistoryStorage/TDengineHistoryStorage.cs

#LineLine coverage
 1using pva.SuperV.Engine.Exceptions;
 2using pva.SuperV.Engine.HistoryRetrieval;
 3using pva.SuperV.Engine.Processing;
 4using TDengine.Driver;
 5using TDengine.Driver.Client;
 6
 7namespace pva.SuperV.Engine.HistoryStorage
 8{
 9    /// <summary>
 10    /// TDengine histiory storage engine.
 11    /// </summary>
 12    public class TDengineHistoryStorage : IHistoryStorageEngine
 13    {
 14        /// <summary>
 15        /// TDengine history storage string.
 16        /// </summary>
 17        public const string Prefix = "TDengine";
 18
 19        /// <summary>
 20        /// Contains the equivalence between .Net and TDengine data types for the types being handled.
 21        /// </summary>
 322        private static readonly Dictionary<Type, string> DotnetToDbTypes = new()
 323        {
 324            { typeof(DateTime), "TIMESTAMP" },
 325            { typeof(short), "SMALLINT"},
 326            { typeof(int), "INT" },
 327            { typeof(long), "BIGINT" },
 328            { typeof(TimeSpan), "BIGINT" },
 329            { typeof(uint), "INT UNSIGNED" },
 330            { typeof(ulong), "BIGINT UNSIGNED" },
 331            { typeof(float), "FLOAT" },
 332            { typeof(double),  "DOUBLE" },
 333            { typeof(bool), "BOOL" },
 334            { typeof(string), "NCHAR(132)" },
 335            { typeof(sbyte),  "TINYINT" },
 336            { typeof(byte), "TINYINT UNSIGNED" },
 337            { typeof(ushort), "SMALLINT UNSIGNED" }
 338            /*
 339            BINARY  byte[]
 340            JSON    byte[]
 341            VARBINARY   byte[]
 342            GEOMETRY    byte[]
 343            */
 344        };
 45
 46        /// <summary>
 47        /// The connection string to the TDengine backend.
 48        /// </summary>
 49        private readonly string connectionString;
 50
 51        /// <summary>
 52        /// The TDengine clinet.
 53        /// </summary>
 54        private ITDengineClient? tdEngineClient;
 55
 56        /// <summary>
 57        /// Builds a TDengine connection from connection stirng.
 58        /// </summary>
 59        /// <param name="tdEngineConnectionString">The TDengine connection string.</param>
 1460        public TDengineHistoryStorage(string tdEngineConnectionString)
 1461        {
 1462            connectionString = tdEngineConnectionString;
 1463            Connect();
 1464        }
 65
 66        /// <summary>
 67        /// Connects to TDengine.
 68        /// </summary>
 69        /// <exception cref="TdEngineException"></exception>
 70        private void Connect()
 1471        {
 1472            var builder = new ConnectionStringBuilder(connectionString);
 73            try
 1474            {
 75                // Open connection with using block, it will close the connection automatically
 1476                tdEngineClient = DbDriver.Open(builder);
 1477            }
 078            catch (Exception e)
 079            {
 080                throw new TdEngineException($"connect to {connectionString}", e);
 81            }
 1482        }
 83
 84        /// <summary>
 85        /// Upsert a history repository in storage engine.
 86        /// </summary>
 87        /// <param name="projectName">Project name to zhich the repository belongs.</param>
 88        /// <param name="repository">History repository</param>
 89        /// <returns>ID of repository in storqge engine.</returns>
 90        public string UpsertRepository(string projectName, HistoryRepository repository)
 1491        {
 1492            string repositoryName = $"{projectName}{repository.Name}".ToLowerInvariant();
 93            try
 1494            {
 1495                tdEngineClient?.Exec($"CREATE DATABASE IF NOT EXISTS {repositoryName} PRECISION 'ns' KEEP 3650 DURATION 
 1496            }
 097            catch (Exception e)
 098            {
 099                throw new TdEngineException($"upsert repository {repositoryName} on {connectionString}", e);
 100            }
 14101            return repositoryName;
 14102        }
 103
 104        /// <summary>
 105        /// Deletes a history repository from storage engine.
 106        /// </summary>
 107        /// <param name="projectName">Project name to zhich the repository belongs.</param>
 108        /// <param name="repositoryName">History repository name.</param>
 109        public void DeleteRepository(string projectName, string repositoryName)
 0110        {
 0111            string repositoryActualName = $"{projectName}{repositoryName}".ToLowerInvariant();
 112            try
 0113            {
 0114                tdEngineClient?.Exec($"DROP DATABASE {repositoryActualName};");
 0115            }
 0116            catch (Exception e)
 0117            {
 0118                throw new TdEngineException($"delete repository {repositoryActualName} on {connectionString}", e);
 119            }
 0120        }
 121
 122        /// <summary>
 123        /// Upsert a class time series in storage engine
 124        /// </summary>
 125        /// <typeparam name="T"></typeparam>
 126        /// <param name="repositoryStorageId">History repository in which the time series should be created.</param>
 127        /// <param name="projectName">Project name to zhich the time series belongs.</param>
 128        /// <param name="className">Class name</param>
 129        /// <param name="historizationProcessing">History processing for which the time series should be created.</param
 130        /// <returns>Time series ID in storage engine.</returns>
 131        public string UpsertClassTimeSerie<T>(string repositoryStorageId, string projectName, string className, Historiz
 28132        {
 28133            string tableName = $"{projectName}{className}{historizationProcessing.Name}".ToLowerInvariant();
 134            try
 28135            {
 28136                tdEngineClient?.Exec($"USE {repositoryStorageId};");
 28137                string fieldNames = "TS TIMESTAMP, QUALITY NCHAR(10),";
 28138                fieldNames +=
 28139                    historizationProcessing.FieldsToHistorize
 173140                        .Select(field => $"_{field.Name} {GetFieldDbType(field)}")
 173141                        .Aggregate((a, b) => $"{a},{b}");
 27142                string command = $"CREATE STABLE IF NOT EXISTS {tableName} ({fieldNames}) TAGS (instance varchar(64));";
 27143                tdEngineClient?.Exec(command);
 27144            }
 1145            catch (SuperVException)
 1146            {
 1147                throw;
 148            }
 0149            catch (Exception e)
 0150            {
 0151                throw new TdEngineException($"upsert class time series {tableName} on {connectionString}", e);
 152            }
 27153            return tableName;
 27154        }
 155
 156        /// <summary>
 157        /// Historize instance values in storage engine
 158        /// </summary>
 159        /// <param name="repositoryStorageId">The history repository ID.</param>
 160        /// <param name="classTimeSerieId">The time series ID.</param>
 161        /// <param name="instanceName">The instance name.</param>
 162        /// <param name="timestamp">the timestamp of the values</param>
 163        /// <param name="fieldsToHistorize">List of fields to be historized.</param>
 164        public void HistorizeValues(string repositoryStorageId, string classTimeSerieId, string instanceName, DateTime t
 19165        {
 19166            string instanceTableName = instanceName.ToLowerInvariant();
 19167            tdEngineClient!.Exec($"USE {repositoryStorageId};");
 19168            using var stmt = tdEngineClient!.StmtInit();
 169            try
 19170            {
 108171                string fieldToHistorizeNames = fieldsToHistorize.Select(field => $"_{field.FieldDefinition!.Name}")
 89172                    .Aggregate((a, b) => $"{a},{b}");
 19173                string fieldsPlaceholders = Enumerable.Repeat("?", fieldsToHistorize.Count + 2)
 127174                    .Aggregate((a, b) => $"{a},{b}");
 19175                string sql = $@"INSERT INTO ? USING {classTimeSerieId} (instance) TAGS(?)
 19176   (TS, QUALITY, {fieldToHistorizeNames}) VALUES ({fieldsPlaceholders});
 19177";
 19178                List<object> rowValues = new(fieldsToHistorize.Count + 2)
 19179                {
 19180                    timestamp.ToLocalTime(),
 19181                    (quality ?? QualityLevel.Good).ToString()
 19182                };
 19183                fieldsToHistorize.ForEach(field =>
 108184                    rowValues.Add(ConvertFieldValueToDb(field)));
 19185                stmt.Prepare(sql);
 186                // set table name
 19187                stmt.SetTableName($"{instanceTableName}");
 188                // set tags
 19189                stmt.SetTags([instanceTableName]);
 190                // bind row values
 19191                stmt.BindRow([.. rowValues]);
 192                // add batch
 19193                stmt.AddBatch();
 194                // execute
 19195                stmt.Exec();
 19196            }
 0197            catch (Exception e)
 0198            {
 0199                throw new TdEngineException($"insert to table {classTimeSerieId} on {connectionString}", e);
 200            }
 38201        }
 202
 203        private static object ConvertFieldValueToDb(IField field)
 89204        {
 89205            return field switch
 89206            {
 6207                Field<bool> typedField => typedField.Value,
 0208                Field<DateTime> typedField => typedField.Value.ToLocalTime(),
 8209                Field<double> typedField => typedField.Value,
 6210                Field<float> typedField => typedField.Value,
 27211                Field<int> typedField => typedField.Value,
 6212                Field<long> typedField => typedField.Value,
 6213                Field<short> typedField => typedField.Value,
 6214                Field<string> typedField => typedField.Value,
 6215                Field<TimeSpan> typedField => typedField.Value.Ticks,
 6216                Field<uint> typedField => typedField.Value,
 6217                Field<ulong> typedField => typedField.Value,
 6218                Field<ushort> typedField => typedField.Value,
 0219                _ => throw new UnhandledMappingException(nameof(TDengineHistoryStorage), field.Type.ToString())
 89220            };
 89221        }
 222
 223        /// <summary>
 224        /// Gets instance values historized between 2 timestamps.
 225        /// </summary>
 226        /// <param name="repositoryStorageId">The history repository ID.</param>
 227        /// <param name="classTimeSerieId">The time series ID.</param>
 228        /// <param name="instanceName">The instance name.</param>
 229        /// <param name="timeRange">Time range for querying.</param>
 230        /// <param name="fields">List of fields to be retrieved. One of them should have the <see cref="HistorizationPro
 231        /// <returns>List of history rows.</returns>
 232        public List<HistoryRow> GetHistoryValues(string repositoryStorageId, string classTimeSerieId, string instanceNam
 8233        {
 8234            string instanceTableName = instanceName.ToLowerInvariant();
 8235            List<HistoryRow> rows = [];
 236            try
 8237            {
 8238                tdEngineClient!.Exec($"USE {repositoryStorageId};");
 60239                string fieldNames = fields.Select(field => $"_{field.Name}")
 52240                    .Aggregate((a, b) => $"{a},{b}");
 8241                string sqlQuery =
 8242                    $@"
 8243SELECT {fieldNames}, TS, QUALITY  FROM {instanceTableName}
 8244 WHERE TS between ""{FormatToSqlDate(timeRange.From)}"" and ""{FormatToSqlDate(timeRange.To)}"";
 8245 ";
 8246                using IRows row = tdEngineClient!.Query(sqlQuery);
 20247                while (row.Read())
 12248                {
 12249                    rows.Add(new HistoryRow(row, fields, true));
 12250                }
 8251            }
 0252            catch (Exception e)
 0253            {
 0254                throw new TdEngineException($"select from table {instanceTableName} on {connectionString}", e);
 255            }
 8256            return rows;
 8257        }
 258
 259        /// <summary>
 260        /// Gets instance statistic values historized between 2 timestamps.
 261        /// </summary>
 262        /// <param name="repositoryStorageId">The history repository ID.</param>
 263        /// <param name="classTimeSerieId">The time series ID.</param>
 264        /// <param name="instanceName">The instance name.</param>
 265        /// <param name="timeRange">Query containing time range parameters.</param>
 266        /// <param name="fields">List of fields to be retrieved. One of them should have the <see cref="HistorizationPro
 267        /// <returns>List of history rows.</returns>
 268        public List<HistoryStatisticRow> GetHistoryStatistics(string repositoryStorageId, string classTimeSerieId, strin
 269            HistoryStatisticTimeRange timeRange, List<HistoryStatisticField> fields)
 5270        {
 5271            string instanceTableName = instanceName.ToLowerInvariant();
 5272            List<HistoryStatisticRow> rows = [];
 273            try
 5274            {
 5275                tdEngineClient!.Exec($"USE {repositoryStorageId};");
 18276                string fieldNames = fields.Select(field => $"{field.StatisticFunction}(_{field.Field.Name})")
 13277                    .Aggregate((a, b) => $"{a},{b}");
 5278                string fillClause = "";
 5279                if (timeRange.FillMode is not null)
 5280                {
 5281                    fillClause = $"FILL({timeRange.FillMode})";
 5282                }
 5283                string sqlQuery =
 5284                    $@"
 5285SELECT {fieldNames}, _WSTART, _WEND, _WDURATION, _WSTART, MAX(QUALITY) FROM {instanceTableName}
 5286 WHERE TS between ""{FormatToSqlDate(timeRange.From)}"" and ""{FormatToSqlDate(timeRange.To)}""
 5287 INTERVAL({FormatInterval(timeRange.Interval)}) SLIDING({FormatInterval(timeRange.Interval)}) {fillClause};
 5288 ";
 5289                using IRows row = tdEngineClient!.Query(sqlQuery);
 11290                while (row.Read())
 6291                {
 6292                    rows.Add(new HistoryStatisticRow(row, fields));
 6293                }
 5294            }
 0295            catch (Exception e)
 0296            {
 0297                throw new TdEngineException($"select from table {instanceTableName} on {connectionString}", e);
 298            }
 5299            return rows;
 5300        }
 301
 302        /// <summary>
 303        /// Formats a DateTime to SQL format used by TDengine.
 304        /// </summary>
 305        /// <param name="dateTime">The date time to be formatted.</param>
 306        /// <returns>SQL string for date time.</returns>
 307        private static string FormatToSqlDate(DateTime dateTime)
 26308        {
 26309            return $"{dateTime.ToUniversalTime():yyyy-MM-dd HH:mm:ss.fffK}";
 26310        }
 311
 312        private static string FormatInterval(TimeSpan interval)
 10313        {
 10314            TimeSpan timespan = interval;
 10315            string intervalText = "";
 10316            intervalText += GetIntervalPeriod(timespan.Days / 365, 'y');
 10317            intervalText += GetIntervalPeriod((timespan.Days % 365) / 30, 'm');
 10318            intervalText += GetIntervalPeriod(((timespan.Days % 365) % 30) / 7, 'w');
 10319            intervalText += GetIntervalPeriod(((timespan.Days % 365) % 30) % 7, 'd');
 10320            intervalText += GetIntervalPeriod(timespan.Hours, 'h');
 10321            intervalText += GetIntervalPeriod(timespan.Minutes, 'm');
 10322            intervalText += GetIntervalPeriod(timespan.Seconds, 's');
 10323            intervalText += GetIntervalPeriod(timespan.Milliseconds, 'a');
 10324            intervalText += GetIntervalPeriod(timespan.Nanoseconds, 'b');
 325            // Remove last comma and space
 10326            return intervalText.TrimEnd()[..^1];
 10327        }
 328
 329        private static string GetIntervalPeriod(int value, char periodLetter)
 90330        {
 90331            if (value > 0)
 10332            {
 10333                return $"{value}{periodLetter}, ";
 334            }
 335
 80336            return "";
 90337        }
 338
 339        /// <summary>
 340        /// Disposes the instance.
 341        /// </summary>
 342        public void Dispose()
 13343        {
 13344            Dispose(true);
 13345            GC.SuppressFinalize(this);
 13346        }
 347
 348        /// <summary>
 349        /// Disposes the instance. Dispose the TDengine connection.
 350        /// </summary>
 351        /// <param name="disposing"></param>
 352        protected virtual void Dispose(bool disposing)
 13353        {
 13354            tdEngineClient?.Dispose();
 13355        }
 356
 357        /// <summary>
 358        /// Gets the TDengine data type for a field definition.
 359        /// </summary>
 360        /// <param name="field">Field for which the TDengine data type should be retrieved.</param>
 361        /// <returns>TDengine data type.</returns>
 362        /// <exception cref="UnhandledHistoryFieldTypeException"></exception>
 363        private static string GetFieldDbType(IFieldDefinition field)
 173364        {
 173365            if (DotnetToDbTypes.TryGetValue(field.Type, out var dbType))
 172366            {
 172367                return dbType;
 368            }
 1369            throw new UnhandledHistoryFieldTypeException(field.Name, field.Type);
 172370        }
 371    }
 372}