< 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_22190969454
Line coverage
86%
Covered lines: 183
Uncovered lines: 28
Coverable lines: 211
Total lines: 382
Line coverage: 86.7%
Branch coverage
84%
Covered branches: 39
Total branches: 46
Branch coverage: 84.7%
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(...)50%4485%
HistorizeValues(...)100%1190%
GetHistoryValues(...)100%2287.5%
GetHistoryStatistics(...)100%6690.32%
Dispose()100%11100%
Dispose(...)50%22100%
GetFieldDbType(...)100%22100%
GetRepositoryName(...)100%11100%
GetClassTimeSerieId(...)100%11100%
GetInstanceTableName(...)100%11100%
FormatToSqlDate(...)100%11100%
FormatInterval(...)100%11100%
GetIntervalPeriod(...)100%22100%
ConvertFieldValueToDb(...)95.83%242493.75%

File(s)

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

#LineLine coverage
 1using pva.SuperV.Common;
 2using pva.SuperV.Engine.Exceptions;
 3using pva.SuperV.Engine.HistoryRetrieval;
 4using pva.SuperV.Engine.Processing;
 5using TDengine.Driver;
 6using TDengine.Driver.Client;
 7
 8namespace pva.SuperV.Engine.HistoryStorage
 9{
 10    /// <summary>
 11    /// TDengine histiory storage engine.
 12    /// </summary>
 13    public class TDengineHistoryStorage : IHistoryStorageEngine
 14    {
 15        /// <summary>
 16        /// TDengine history storage string.
 17        /// </summary>
 18        public const string Prefix = "TDengine";
 19
 20        /// <summary>
 21        /// Contains the equivalence between .Net and TDengine data types for the types being handled.
 22        /// </summary>
 323        private static readonly Dictionary<Type, string> DotnetToDbTypes = new()
 324        {
 325            { typeof(DateTime), "TIMESTAMP" },
 326            { typeof(short), "SMALLINT"},
 327            { typeof(int), "INT" },
 328            { typeof(long), "BIGINT" },
 329            { typeof(TimeSpan), "BIGINT" },
 330            { typeof(uint), "INT UNSIGNED" },
 331            { typeof(ulong), "BIGINT UNSIGNED" },
 332            { typeof(float), "FLOAT" },
 333            { typeof(double),  "DOUBLE" },
 334            { typeof(bool), "BOOL" },
 335            { typeof(string), "NCHAR(132)" },
 336            { typeof(sbyte),  "TINYINT" },
 337            { typeof(byte), "TINYINT UNSIGNED" },
 338            { typeof(ushort), "SMALLINT UNSIGNED" }
 339            /*
 340            BINARY  byte[]
 341            JSON    byte[]
 342            VARBINARY   byte[]
 343            GEOMETRY    byte[]
 344            */
 345        };
 46
 47        /// <summary>
 48        /// The connection string to the TDengine backend.
 49        /// </summary>
 50        private readonly string connectionString;
 51
 52        /// <summary>
 53        /// The TDengine clinet.
 54        /// </summary>
 55        private ITDengineClient? tdEngineClient;
 56
 57        /// <summary>
 58        /// Builds a TDengine connection from connection stirng.
 59        /// </summary>
 60        /// <param name="tdEngineConnectionString">The TDengine connection string.</param>
 1261        public TDengineHistoryStorage(string tdEngineConnectionString)
 1262        {
 1263            connectionString = tdEngineConnectionString;
 1264            Connect();
 1265        }
 66
 67        /// <summary>
 68        /// Connects to TDengine.
 69        /// </summary>
 70        /// <exception cref="TdEngineException"></exception>
 71        private void Connect()
 1272        {
 1273            var builder = new ConnectionStringBuilder(connectionString);
 74            try
 1275            {
 76                // Open connection with using block, it will close the connection automatically
 1277                tdEngineClient = DbDriver.Open(builder);
 1278            }
 079            catch (Exception e)
 080            {
 081                throw new TdEngineException($"connect to {connectionString}", e);
 82            }
 1283        }
 84
 85        /// <summary>
 86        /// Upsert a history repository in storage engine.
 87        /// </summary>
 88        /// <param name="projectName">Project name to hwich the repository belongs.</param>
 89        /// <param name="repository">History repository</param>
 90        /// <returns>ID of repository in storage engine.</returns>
 91        /// <exception cref="TdEngineException">TDengine error</exception>
 92        public string UpsertRepository(string projectName, HistoryRepository repository)
 1293        {
 1294            string repositoryName = GetRepositoryName(projectName, repository.Name);
 95            try
 1296            {
 1297                tdEngineClient?.Exec($"CREATE DATABASE IF NOT EXISTS {repositoryName} PRECISION 'ns' KEEP 3650 DURATION 
 1298            }
 099            catch (Exception e)
 0100            {
 0101                throw new TdEngineException($"upsert repository {repositoryName} on {connectionString}", e);
 102            }
 12103            return repositoryName;
 12104        }
 105
 106        /// <summary>
 107        /// Deletes a history repository from storage engine.
 108        /// </summary>
 109        /// <param name="projectName">Project name to zhich the repository belongs.</param>
 110        /// <param name="repositoryName">History repository name.</param>
 111        /// <exception cref="TdEngineException">TDengine error</exception>
 112        public void DeleteRepository(string projectName, string repositoryName)
 0113        {
 0114            string repositoryActualName = GetRepositoryName(projectName, repositoryName);
 115            try
 0116            {
 0117                tdEngineClient?.Exec($"DROP DATABASE {repositoryActualName};");
 0118            }
 0119            catch (Exception e)
 0120            {
 0121                throw new TdEngineException($"delete repository {repositoryActualName} on {connectionString}", e);
 122            }
 0123        }
 124
 125        /// <summary>
 126        /// Upsert a class time series in storage engine
 127        /// </summary>
 128        /// <param name="repositoryStorageId">History repository in which the time series should be created.</param>
 129        /// <param name="projectName">Project name to zhich the time series belongs.</param>
 130        /// <param name="className">Class name</param>
 131        /// <param name="historizationProcessing">History processing for which the time series should be created.</param
 132        /// <returns>Time series ID in storage engine.</returns>
 133        /// <exception cref="TdEngineException">TDengine error</exception>
 134        public string UpsertClassTimeSerie(string repositoryStorageId, string projectName, string className, IHistorizat
 151135        {
 151136            string classTimeSerieId = GetClassTimeSerieId(projectName, className, historizationProcessing);
 137            try
 151138            {
 151139                tdEngineClient?.Exec($"USE {repositoryStorageId};");
 151140                string fieldNames = "TS TIMESTAMP, QUALITY NCHAR(10),";
 151141                fieldNames +=
 151142                    historizationProcessing.FieldsToHistorize
 164143                        .Select(field => $"_{field.Name} {GetFieldDbType(field)}")
 164144                        .Aggregate((a, b) => $"{a},{b}");
 150145                string command = $"CREATE STABLE IF NOT EXISTS {classTimeSerieId} ({fieldNames}) TAGS (instance varchar(
 150146                tdEngineClient?.Exec(command);
 150147            }
 1148            catch (SuperVException)
 1149            {
 1150                throw;
 151            }
 0152            catch (Exception e)
 0153            {
 0154                throw new TdEngineException($"upsert class time series {classTimeSerieId} on {connectionString}", e);
 155            }
 150156            return classTimeSerieId;
 150157        }
 158
 159        /// <summary>
 160        /// Historize instance values in storage engine
 161        /// </summary>
 162        /// <param name="repositoryStorageId">The history repository ID.</param>
 163        /// <param name="historizationProcessingName">The historization processing name.</param>
 164        /// <param name="classTimeSerieId">The time series ID.</param>
 165        /// <param name="instanceName">The instance name.</param>
 166        /// <param name="timestamp">the timestamp of the values</param>
 167        /// <param name="quality">The quality level of the values.</param>
 168        /// <param name="fieldsToHistorize">List of fields to be historized.</param>
 169        /// <exception cref="TdEngineException">TDengine error</exception>
 170        public void HistorizeValues(string repositoryStorageId, string historizationProcessingName, string classTimeSeri
 65171        {
 65172            string instanceTableName = $"{instanceName}_{historizationProcessingName}".ToLowerInvariant();
 65173            tdEngineClient!.Exec($"USE {repositoryStorageId};");
 65174            using var stmt = tdEngineClient!.StmtInit();
 175            try
 65176            {
 186177                string fieldToHistorizeNames = fieldsToHistorize.Select(field => $"_{field.FieldDefinition!.Name}")
 121178                    .Aggregate((a, b) => $"{a},{b}");
 65179                string fieldValuesPlaceholders = Enumerable.Repeat("?", fieldsToHistorize.Count + 2)
 251180                    .Aggregate((a, b) => $"{a},{b}");
 65181                string sql = $@"INSERT INTO ? USING {classTimeSerieId} (instance) TAGS(?)
 65182   (TS, QUALITY, {fieldToHistorizeNames}) VALUES ({fieldValuesPlaceholders});
 65183";
 65184                List<object> rowValues = new(fieldsToHistorize.Count + 2)
 65185                {
 65186                    timestamp.ToLocalTime(),
 65187                    (quality ?? QualityLevel.Good).ToString()
 65188                };
 65189                fieldsToHistorize.ForEach(field =>
 186190                    rowValues.Add(ConvertFieldValueToDb(field)));
 65191                stmt.Prepare(sql);
 192                // set table name
 65193                stmt.SetTableName(instanceTableName);
 194                // set tags
 65195                stmt.SetTags([instanceTableName]);
 196                // bind row values
 65197                stmt.BindRow([.. rowValues]);
 198                // add batch
 65199                stmt.AddBatch();
 200                // execute
 65201                stmt.Exec();
 65202            }
 0203            catch (Exception e)
 0204            {
 0205                throw new TdEngineException($"insert to table {classTimeSerieId} on {connectionString}", e);
 206            }
 130207        }
 208
 209        /// <summary>
 210        /// Gets instance values historized between 2 timestamps.
 211        /// </summary>
 212        /// <param name="instanceName">The instance name.</param>
 213        /// <param name="timeRange">Time range for querying.</param>
 214        /// <param name="instanceTimeSerieParameters">Parameters defining the time serie.</param>
 215        /// <param name="fields">List of fields to be retrieved. One of them should have the <see cref="HistorizationPro
 216        /// <returns>List of history rows.</returns>
 217        /// <exception cref="TdEngineException">TDengine error</exception>
 218        public List<HistoryRow> GetHistoryValues(string instanceName, HistoryTimeRange timeRange, InstanceTimeSerieParam
 30219        {
 30220            string instanceTableName = GetInstanceTableName(instanceName, instanceTimeSerieParameters);
 30221            List<HistoryRow> rows = [];
 222            try
 30223            {
 30224                tdEngineClient!.Exec($"USE {instanceTimeSerieParameters.HistorizationProcessing!.HistoryRepository!.Hist
 82225                string fieldNames = fields.Select(field => $"_{field.Name}")
 52226                    .Aggregate((a, b) => $"{a},{b}");
 30227                string sqlQuery =
 30228                    $"""
 30229SELECT {fieldNames}, TS, QUALITY  FROM {instanceTableName}
 30230 WHERE TS between "{FormatToSqlDate(timeRange.From)}" and "{FormatToSqlDate(timeRange.To)}";
 30231
 30232""";
 30233                using IRows row = tdEngineClient!.Query(sqlQuery);
 64234                while (row.Read())
 34235                {
 34236                    rows.Add(new HistoryRow(row, fields, true));
 34237                }
 30238            }
 0239            catch (Exception e)
 0240            {
 0241                throw new TdEngineException($"select from table {instanceTableName} on {connectionString}", e);
 242            }
 30243            return rows;
 30244        }
 245
 246        /// <summary>
 247        /// Gets instance statistic values historized between 2 timestamps.
 248        /// </summary>
 249        /// <param name="instanceName">The instance name.</param>
 250        /// <param name="timeRange">Query containing time range parameters.</param>
 251        /// <param name="instanceTimeSerieParameters">Parameters defining the time serie.</param>
 252        /// <param name="fields">List of fields to be retrieved. One of them should have the <see cref="HistorizationPro
 253        /// <returns>List of history rows.</returns>
 254        /// <exception cref="TdEngineException">TDengine error</exception>
 255        public List<HistoryStatisticRow> GetHistoryStatistics(string instanceName, HistoryStatisticTimeRange timeRange,
 256            InstanceTimeSerieParameters instanceTimeSerieParameters, List<HistoryStatisticField> fields)
 25257        {
 25258            string instanceTableName = GetInstanceTableName(instanceName, instanceTimeSerieParameters);
 25259            List<HistoryStatisticRow> rows = [];
 260            try
 25261            {
 25262                tdEngineClient!.Exec($"USE {instanceTimeSerieParameters.HistorizationProcessing!.HistoryRepository!.Hist
 58263                string fieldNames = fields.Select(field => field.StatisticFunction == HistoryStatFunction.NONE ?
 58264                $"_{field.Field.Name}" : $"{field.StatisticFunction}(_{field.Field.Name})")
 33265                    .Aggregate((a, b) => $"{a},{b}");
 25266                string fillClause = "";
 25267                if (timeRange.FillMode is not null)
 25268                {
 25269                    fillClause = $"FILL({timeRange.FillMode})";
 25270                }
 25271                string sqlQuery =
 25272                    $"""
 25273SELECT {fieldNames}, _WSTART, _WEND, _WDURATION, _WSTART, MAX(QUALITY) FROM {instanceTableName}
 25274 WHERE TS between "{FormatToSqlDate(timeRange.From)}" and "{FormatToSqlDate(timeRange.To)}"
 25275 INTERVAL({FormatInterval(timeRange.Interval)}) SLIDING({FormatInterval(timeRange.Interval)}) {fillClause};
 25276
 25277""";
 25278                using IRows row = tdEngineClient!.Query(sqlQuery);
 51279                while (row.Read())
 26280                {
 26281                    rows.Add(new HistoryStatisticRow(row, fields, false));
 26282                }
 25283            }
 0284            catch (Exception e)
 0285            {
 0286                throw new TdEngineException($"select from table {instanceTableName} on {connectionString}", e);
 287            }
 25288            return rows;
 25289        }
 290
 291        /// <summary>
 292        /// Disposes the instance.
 293        /// </summary>
 294        public void Dispose()
 10295        {
 10296            Dispose(true);
 10297            GC.SuppressFinalize(this);
 10298        }
 299
 300        /// <summary>
 301        /// Disposes the instance. Dispose the TDengine connection.
 302        /// </summary>
 303        /// <param name="disposing">Indicates if called from Dispose()</param>
 304        protected virtual void Dispose(bool disposing)
 10305        {
 10306            tdEngineClient?.Dispose();
 10307        }
 308
 309        /// <summary>
 310        /// Gets the TDengine data type for a field definition.
 311        /// </summary>
 312        /// <param name="field">Field for which the TDengine data type should be retrieved.</param>
 313        /// <returns>TDengine data type.</returns>
 314        /// <exception cref="UnhandledHistoryFieldTypeException"></exception>
 315        private static string GetFieldDbType(IFieldDefinition field)
 164316        {
 164317            if (DotnetToDbTypes.TryGetValue(field.Type, out var dbType))
 163318            {
 163319                return dbType;
 320            }
 1321            throw new UnhandledHistoryFieldTypeException(field.Name, field.Type);
 163322        }
 323
 324        private static string GetRepositoryName(string projectName, string repositoryName)
 12325            => $"{projectName}_{repositoryName}".ToLowerInvariant();
 326
 327        private static string GetClassTimeSerieId(string projectName, string className, IHistorizationProcessing histori
 151328            => $"{projectName}_{className}_{historizationProcessing.Name}".ToLowerInvariant();
 329
 330        private static string GetInstanceTableName(string instanceName, InstanceTimeSerieParameters instanceTimeSeriePar
 55331            => $"{instanceName}_{instanceTimeSerieParameters!.HistorizationProcessing!.Name}".ToLowerInvariant();
 332
 333        /// <summary>
 334        /// Formats a DateTime to SQL format used by TDengine.
 335        /// </summary>
 336        /// <param name="dateTime">The date time to be formatted.</param>
 337        /// <returns>SQL string for date time.</returns>
 338        private static string FormatToSqlDate(DateTime dateTime)
 110339        {
 110340            return $"{dateTime.ToUniversalTime():yyyy-MM-dd HH:mm:ss.fffK}";
 110341        }
 342
 343        private static string FormatInterval(TimeSpan interval)
 50344        {
 50345            TimeSpan timespan = interval;
 50346            string intervalText = "";
 50347            intervalText += GetIntervalPeriod(timespan.Days / 365, 'y');
 50348            intervalText += GetIntervalPeriod((timespan.Days % 365) / 30, 'm');
 50349            intervalText += GetIntervalPeriod((timespan.Days % 365 % 30) / 7, 'w');
 50350            intervalText += GetIntervalPeriod(timespan.Days % 365 % 30 % 7, 'd');
 50351            intervalText += GetIntervalPeriod(timespan.Hours, 'h');
 50352            intervalText += GetIntervalPeriod(timespan.Minutes, 'm');
 50353            intervalText += GetIntervalPeriod(timespan.Seconds, 's');
 50354            intervalText += GetIntervalPeriod(timespan.Milliseconds, 'a');
 50355            intervalText += GetIntervalPeriod(timespan.Nanoseconds, 'b');
 356            // Remove last comma and space
 50357            return intervalText.TrimEnd()[..^1];
 50358        }
 359
 360        private static string GetIntervalPeriod(int value, char periodLetter)
 450361            => value > 0 ? $"{value}{periodLetter}, " : "";
 362
 363        private static object ConvertFieldValueToDb(IField field)
 121364            => field switch
 121365            {
 8366                Field<bool> typedField => typedField.Value,
 4367                Field<DateTime> typedField => typedField.Value.ToLocalTime(),
 14368                Field<double> typedField => typedField.Value,
 8369                Field<float> typedField => typedField.Value,
 33370                Field<int> typedField => typedField.Value,
 8371                Field<long> typedField => typedField.Value,
 8372                Field<short> typedField => typedField.Value,
 8373                Field<string> typedField => typedField.Value,
 6374                Field<TimeSpan> typedField => typedField.Value.Ticks,
 8375                Field<uint> typedField => typedField.Value,
 8376                Field<ulong> typedField => typedField.Value,
 8377                Field<ushort> typedField => typedField.Value,
 0378                _ => throw new UnhandledMappingException(nameof(TDengineHistoryStorage), field.Type.ToString())
 121379            };
 380
 381    }
 382}