• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

HicServices / RDMP / 8598528338

08 Apr 2024 10:28AM UTC coverage: 56.914% (+0.08%) from 56.837%
8598528338

push

github

web-flow
Move 8.1.5 to main for autoupdater (#1797)

* bump to net8

* add sqlcli

* Bump Autoupdater.NET.Official from 1.8.4 to 1.8.5

Bumps [Autoupdater.NET.Official](https://github.com/ravibpatel/AutoUpdater.NET) from 1.8.4 to 1.8.5.
- [Release notes](https://github.com/ravibpatel/AutoUpdater.NET/releases)
- [Commits](https://github.com/ravibpatel/AutoUpdater.NET/compare/v1.8.4...v1.8.5)

---
updated-dependencies:
- dependency-name: Autoupdater.NET.Official
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Delete .nuget/packages.config (#1758)

* Delete .nuget/packages.config

Remove  hopefully obsolete package list confusing dependency tracking

* Remove .nuget folder

---------

Co-authored-by: James A Sutherland <>

* Bump svenstaro/upload-release-action from 2.7.0 to 2.9.0 (#1759)

* Bump shogo82148/actions-setup-perl from 1.28.0 to 1.29.0 (#1751)

Bumps [shogo82148/actions-setup-perl](https://github.com/shogo82148/actions-setup-perl) from 1.28.0 to 1.29.0.
- [Release notes](https://github.com/shogo82148/actions-setup-perl/releases)
- [Commits](https://github.com/shogo82148/actions-setup-perl/compare/v1.28.0...v1.29.0)

---
updated-dependencies:
- dependency-name: shogo82148/actions-setup-perl
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: James Friel <jfriel001@dundee.ac.uk>

* Bump CsvHelper from 30.0.3 to 30.1.0 (#1737)

Bumps [CsvHelper](https://github.com/JoshClose/CsvHelper) from 30.0.3 to 30.1.0.
- [Commits](https://github.com/JoshClose/CsvHelper/compare/30.0.3...30.1.0)

---
updated-dependencies:
- dependency-name: CsvHelper
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

S... (continued)

10835 of 20509 branches covered (52.83%)

Branch coverage included in aggregate %.

99 of 176 new or added lines in 33 files covered. (56.25%)

10 existing lines in 7 files now uncovered.

30856 of 52744 relevant lines covered (58.5%)

7400.67 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

44.39
/Rdmp.Core/Curation/Data/DataLoad/ProcessTask.cs
1
// Copyright (c) The University of Dundee 2018-2019
2
// This file is part of the Research Data Management Platform (RDMP).
3
// RDMP is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
4
// RDMP is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
5
// You should have received a copy of the GNU General Public License along with RDMP. If not, see <https://www.gnu.org/licenses/>.
6

7
using System;
8
using System.Collections.Generic;
9
using System.Data.Common;
10
using System.IO;
11
using System.Linq;
12
using System.Text.RegularExpressions;
13
using Rdmp.Core.Curation.Data.Cohort;
14
using Rdmp.Core.Curation.Data.ImportExport;
15
using Rdmp.Core.Curation.Data.Serialization;
16
using Rdmp.Core.MapsDirectlyToDatabaseTable;
17
using Rdmp.Core.MapsDirectlyToDatabaseTable.Attributes;
18
using Rdmp.Core.Repositories;
19
using Rdmp.Core.ReusableLibraryCode.Annotations;
20
using Rdmp.Core.ReusableLibraryCode.Checks;
21

22
namespace Rdmp.Core.Curation.Data.DataLoad;
23

24
/// <summary>
25
/// Describes a specific operation carried out at a specific step of a LoadMetadata.  This could be 'unzip all files called *.zip in for loading' or
26
/// 'after loading the data to live, call sp_clean_table1' or 'Connect to webservice X and download 1,000,000 records which will be serialized into XML'
27
/// 
28
/// <para>The class achieves this wide ranging functionality through the interaction of ProcessTaskType and Path.  e.g. when ProcessTaskType is Attacher then
29
/// Path functions as the Type name of a class that implements IAttacher e.g. 'LoadModules.Generic.Attachers.AnySeparatorFileAttacher'.  </para>
30
/// 
31
/// <para>Each ProcessTask can have one or more strongly typed arguments (see entity ProcessTaskArgument), these are discovered at design time by using
32
/// reflection to query the Path e.g. 'AnySeparatorFileAttacher' for all properties marked with [DemandsInitialization] attribute.  This allows for 3rd party developers
33
/// to write plugin classes to easily handle proprietary/bespoke source file types or complex data load requirements.</para>
34
/// </summary>
35
public class ProcessTask : DatabaseEntity, IProcessTask, IOrderable, INamed, ICheckable
36
{
37
    #region Database Properties
38

39
    private int _loadMetadataID;
40
    private int? _relatesSolelyToCatalogueID;
41
    private int _order;
42
    private string _path;
43
    private string _name;
44
    private LoadStage _loadStage;
45
    private ProcessTaskType _processTaskType;
46
    private bool _isDisabled;
47
#nullable enable
48
    private string? _SerialisableConfiguration;
49
#nullable disable
50
    /// <summary>
51
    /// The load the process task exists as part of
52
    /// </summary>
53
    [Relationship(typeof(LoadMetadata), RelationshipType.SharedObject)]
54
    public int LoadMetadata_ID
55
    {
56
        get => _loadMetadataID;
2,252✔
57
        set => SetField(ref _loadMetadataID, value);
784✔
58
    }
59

60
    /// <inheritdoc/>
61
    [Obsolete(
62
        "Since you can't change which Catalogues are loaded by a LoadMetadata at runtime, this property is now obsolete")]
63
    public int? RelatesSolelyToCatalogue_ID
64
    {
65
        get => _relatesSolelyToCatalogueID;
536✔
66
        set => SetField(ref _relatesSolelyToCatalogueID, value);
146✔
67
    }
68

69
    /// <inheritdoc/>
70
    public int Order
71
    {
72
        get => _order;
1,112✔
73
        set => SetField(ref _order, value);
794✔
74
    }
75

76
    /// <inheritdoc/>
77
    [AdjustableLocation]
78
    public string Path
79
    {
80
        get => _path;
1,036✔
81
        set => SetField(ref _path, value);
862✔
82
    }
83

84
    /// <inheritdoc cref="IProcessTask.Name"/>
85
    [NotNull]
86
    public string Name
87
    {
88
        get => _name;
782✔
89
        set => SetField(ref _name, value);
858✔
90
    }
91

92
    /// <inheritdoc/>
93
    public LoadStage LoadStage
94
    {
95
        get => _loadStage;
1,662✔
96
        set => SetField(ref _loadStage, value);
810✔
97
    }
98

99
    /// <inheritdoc/>
100
    public ProcessTaskType ProcessTaskType
101
    {
102
        get => _processTaskType;
884✔
103
        set => SetField(ref _processTaskType, value);
904✔
104
    }
105

106
    /// <inheritdoc/>
107
    public bool IsDisabled
108
    {
109
        get => _isDisabled;
1,156✔
110
        set => SetField(ref _isDisabled, value);
740✔
111
    }
112

113

114
    /// <inheritdoc/>
115
#nullable enable
116
    public string? SerialisableConfiguration
117
    {
118
        get => _SerialisableConfiguration;
632✔
119
        set => SetField(ref _SerialisableConfiguration, value);
730✔
120
    }
121
#nullable disable
122

123
    #endregion
124

125
    #region Relationships
126

127
    /// <inheritdoc cref="LoadMetadata_ID"/>
128
    [NoMappingToDatabase]
129
    public LoadMetadata LoadMetadata => Repository.GetObjectByID<LoadMetadata>(LoadMetadata_ID);
168✔
130

131
    /// <inheritdoc/>
132
    [NoMappingToDatabase]
133
    public IEnumerable<ProcessTaskArgument> ProcessTaskArguments =>
134
        Repository.GetAllObjectsWithParent<ProcessTaskArgument>(this);
414✔
135

136
    /// <summary>
137
    /// All <see cref="ILoadProgress"/> (if any) that can be advanced by executing this load.  This allows batch execution of large loads
138
    /// </summary>
139
    [NoMappingToDatabase]
140
    public ILoadProgress[] LoadProgresses => LoadMetadata.LoadProgresses;
2✔
141

142
    #endregion
143

144
    public ProcessTask()
×
145
    {
146
    }
×
147

148
    /// <summary>
149
    /// Creates a new operation in the data load (e.g. copy files from A to B, load all CSV files to RAW table B etc)
150
    /// </summary>
151
    /// <param name="repository"></param>
152
    /// <param name="parent"></param>
153
    /// <param name="stage"></param>
154
    public ProcessTask(ICatalogueRepository repository, ILoadMetadata parent, LoadStage stage)
190✔
155
    {
156
        var order =
190✔
157
            repository.GetAllObjectsWithParent<ProcessTask>(parent).Select(t => t.Order).DefaultIfEmpty().Max() + 1;
202✔
158

159
        repository.InsertAndHydrate(this, new Dictionary<string, object>
190✔
160
        {
190✔
161
            { "LoadMetadata_ID", parent.ID },
190✔
162
            { "ProcessTaskType", ProcessTaskType.Executable.ToString() },
190✔
163
            { "LoadStage", stage },
190✔
164
            { "Name", $"New Process{Guid.NewGuid()}" },
190✔
165
            { "Order", order },
190✔
166
        });
190✔
167
    }
190✔
168

169
    /// <summary>
170
    /// Creates a new operation in the data load (e.g. copy files from A to B, load all CSV files to RAW table B etc)
171
    /// </summary>
172
    /// <param name="repository"></param>
173
    /// <param name="parent"></param>
174
    /// <param name="stage"></param>
175
    /// <param name="serialisableConfiguration"></param>
NEW
176
    public ProcessTask(ICatalogueRepository repository, ILoadMetadata parent, LoadStage stage, string serialisableConfiguration = null)
×
177
    {
NEW
178
        var order =
×
NEW
179
            repository.GetAllObjectsWithParent<ProcessTask>(parent).Select(t => t.Order).DefaultIfEmpty().Max() + 1;
×
180

NEW
181
        repository.InsertAndHydrate(this, new Dictionary<string, object>
×
NEW
182
        {
×
NEW
183
            { "LoadMetadata_ID", parent.ID },
×
NEW
184
            { "ProcessTaskType", ProcessTaskType.Executable.ToString() },
×
NEW
185
            { "LoadStage", stage },
×
NEW
186
            { "Name", $"New Process{Guid.NewGuid()}" },
×
NEW
187
            { "Order", order },
×
NEW
188
            {"SerialisableConfiguration", serialisableConfiguration}
×
UNCOV
189
        });
×
UNCOV
190
    }
×
191

192
    internal ProcessTask(ICatalogueRepository repository, DbDataReader r)
193
        : base(repository, r)
584✔
194
    {
195
        LoadMetadata_ID = int.Parse(r["LoadMetaData_ID"].ToString());
584✔
196

197
        if (r["RelatesSolelyToCatalogue_ID"] != DBNull.Value)
584!
198
            _relatesSolelyToCatalogueID = int.Parse(r["RelatesSolelyToCatalogue_ID"].ToString());
×
199

200
        Path = r["Path"] as string;
584✔
201
        Name = r["Name"] as string;
584✔
202
        Order = int.Parse(r["Order"].ToString());
584✔
203

204
        if (Enum.TryParse(r["ProcessTaskType"] as string, out ProcessTaskType processTaskType))
584!
205
            ProcessTaskType = processTaskType;
584✔
206
        else
207
            throw new Exception($"Could not parse ProcessTaskType:{r["ProcessTaskType"]}");
×
208

209
        if (Enum.TryParse(r["LoadStage"] as string, out LoadStage loadStage))
584!
210
            LoadStage = loadStage;
584✔
211
        else
212
            throw new Exception($"Could not parse LoadStage:{r["LoadStage"]}");
×
213

214
        IsDisabled = Convert.ToBoolean(r["IsDisabled"]);
584✔
215
        if(r["SerialisableConfiguration"] is not null)
584✔
216
            SerialisableConfiguration = r["SerialisableConfiguration"].ToString();
584✔
217
    }
584✔
218

219
    internal ProcessTask(ShareManager shareManager, ShareDefinition shareDefinition)
6✔
220
    {
221
        shareManager.UpsertAndHydrate(this, shareDefinition);
6✔
222
    }
6✔
223

224
    /// <inheritdoc/>
225
    public override string ToString() => Name;
70✔
226

227
    /// <inheritdoc/>
228
    public void Check(ICheckNotifier notifier)
229
    {
230
        switch (ProcessTaskType)
38!
231
        {
232
            case ProcessTaskType.Executable:
233
                CheckFileExistenceAndUniqueness(notifier);
×
234
                break;
×
235
            case ProcessTaskType.SQLFile:
236
                CheckFileExistenceAndUniqueness(notifier);
×
237
                CheckForProblemsInSQLFile(notifier);
×
238
                break;
×
239
            case ProcessTaskType.SQLBakFile:
240
                CheckFileExistenceAndUniqueness(notifier);
×
241
                break;
×
242
            case ProcessTaskType.Attacher:
243
                break;
244
            case ProcessTaskType.DataProvider:
245
                break;
246
            case ProcessTaskType.MutilateDataTable:
247
                break;
248
            default:
249
                throw new ArgumentOutOfRangeException();
×
250
        }
251
    }
38✔
252

253
    private void CheckForProblemsInSQLFile(ICheckNotifier notifier)
254
    {
255
        try
256
        {
257
            var sql = File.ReadAllText(Path);
×
258

259
            //let's check for any SQL that indicates user is trying to modify a STAGING table in a RAW script (for example)
260
            foreach (var tableInfo in LoadMetadata.GetDistinctTableInfoList(false))
×
261
                //for each stage get all the object names that are in that stage
262
                foreach (var stage in new[] { LoadStage.AdjustRaw, LoadStage.AdjustStaging, LoadStage.PostLoad })
×
263
                {
264
                    //process task belongs in that stage anyway so nothing is prohibited
265
                    if (stage == (LoadStage == LoadStage.Mounting ? LoadStage.AdjustRaw : LoadStage))
×
266
                        continue;
267

268
                    //figure out what is prohibited
269
                    var prohibitedSql = tableInfo.GetQuerySyntaxHelper()
×
270
                        .EnsureFullyQualified(tableInfo.GetDatabaseRuntimeName(stage), null,
×
271
                            tableInfo.GetRuntimeName(stage));
×
272

273
                    //if we reference it, complain
274
                    if (sql.Contains(prohibitedSql))
×
275
                        notifier.OnCheckPerformed(
×
276
                            new CheckEventArgs(
×
277
                                $"Sql in file '{Path}' contains a reference to '{prohibitedSql}' which is prohibited since the ProcessTask ('{Name}') runs in LoadStage {LoadStage}",
×
278
                                CheckResult.Warning));
×
279
                }
280
        }
×
281
        catch (Exception e)
×
282
        {
283
            notifier.OnCheckPerformed(new CheckEventArgs($"Failed to check the contents of the SQL file '{Path}'",
×
284
                CheckResult.Fail, e));
×
285
        }
×
286
    }
×
287

288
    private void CheckFileExistenceAndUniqueness(ICheckNotifier notifier)
289
    {
290
        if (string.IsNullOrWhiteSpace(Path))
×
291
        {
292
            notifier.OnCheckPerformed(new CheckEventArgs($"No Path specified for ProcessTask '{Name}'",
×
293
                CheckResult.Fail));
×
294
            return;
×
295
        }
296

297
        notifier.OnCheckPerformed(!File.Exists(Path)
×
298
            ? new CheckEventArgs($"Could not find File '{Path}' for ProcessTask '{Name}'", CheckResult.Fail)
×
299
            : new CheckEventArgs($"Found File '{Path}'", CheckResult.Success));
×
300

301

302
        var matchingPaths = Repository.GetAllObjects<ProcessTask>().Where(pt => pt.Path.Equals(Path));
×
303
        foreach (var duplicate in matchingPaths.Except(new[] { this }))
×
304
            notifier.OnCheckPerformed(
×
305
                new CheckEventArgs(
×
306
                    $"ProcessTask '{duplicate}' (ID={duplicate.ID}) also uses file '{System.IO.Path.GetFileName(Path)}'",
×
307
                    CheckResult.Warning));
×
308

309
        //conflicting tokens in Name string
310
        foreach (Match match in Regex.Matches(Name, @"'.*((\.exe')|(\.sql'))"))
×
311
            if (match.Success)
×
312
            {
313
                var referencedFile = System.IO.Path.GetFileName(match.Value.Trim('\''));
×
314
                var actualFile = System.IO.Path.GetFileName(Path);
×
315

316
                if (referencedFile != actualFile)
×
317
                    notifier.OnCheckPerformed(
×
318
                        new CheckEventArgs(
×
319
                            $"Name of ProcessTask '{Name}' (ID={ID}) references file '{match.Value}' but the Path of the ProcessTask is '{Path}'",
×
320
                            CheckResult.Fail));
×
321
            }
322
    }
×
323

324
    /// <summary>
325
    /// Returns all tables loaded by the parent <see cref="LoadMetadata"/>
326
    /// </summary>
327
    /// <returns></returns>
328
    public IEnumerable<TableInfo> GetTableInfos() => LoadMetadata.GetDistinctTableInfoList(true);
2✔
329

330
    /// <inheritdoc/>
331
    public IEnumerable<IArgument> GetAllArguments() => ProcessTaskArguments;
276✔
332

333
    /// <inheritdoc/>
334
    public IArgument CreateNewArgument() => new ProcessTaskArgument((ICatalogueRepository)Repository, this);
1,198✔
335

336
    /// <inheritdoc/>
337
    public string GetClassNameWhoArgumentsAreFor() => Path;
18✔
338

339
    /// <summary>
340
    /// Creates a new copy of the processTask and all its arguments in the database, this clone is then hooked up to the
341
    /// new LoadMetadata at the specified stage
342
    /// </summary>
343
    /// <param name="loadMetadata">The new LoadMetadata parent for the clone</param>
344
    /// <param name="loadStage">The new load stage to put the clone in </param>
345
    /// <returns>the new ProcessTask (the clone has a different ID to the parent)</returns>
346
    public ProcessTask CloneToNewLoadMetadataStage(LoadMetadata loadMetadata, LoadStage loadStage)
347
    {
348
        var cataRepository = (ICatalogueRepository)Repository;
4✔
349

350
        //clone only accepts sql connections so make sure we aren't in mysql land or something
351
        using (cataRepository.BeginNewTransaction())
4✔
352
        {
353
            try
354
            {
355
                //get list of arguments to also clone (will happen outside of transaction
356
                var toCloneArguments = ProcessTaskArguments.ToArray();
4✔
357

358
                //create a new transaction for all the cloning - note that once all objects are cloned the transaction is committed then all the objects are adjusted outside the transaction
359
                var clone = new ProcessTask(CatalogueRepository, LoadMetadata, loadStage);
4✔
360
                CopyShallowValuesTo(clone);
4✔
361

362
                //foreach of our child arguments
363
                foreach (var argument in toCloneArguments)
12✔
364
                    //clone it but rewire it to the proper ProcessTask parent (the clone)
365
                    argument.ShallowClone(clone);
2✔
366

367
                //the values passed into parameter
368
                clone.LoadMetadata_ID = loadMetadata.ID;
4✔
369
                clone.LoadStage = loadStage;
4✔
370
                clone.SaveToDatabase();
4✔
371

372
                //it worked
373
                cataRepository.EndTransaction(true);
4✔
374

375
                //return the clone
376
                return clone;
4✔
377
            }
378
            catch (Exception)
×
379
            {
380
                cataRepository.EndTransaction(false);
×
381
                throw;
×
382
            }
383
        }
384
    }
4✔
385

386
    /// <inheritdoc/>
387
    public IArgument[] CreateArgumentsForClassIfNotExists(Type t) =>
388
        ArgumentFactory.CreateArgumentsForClassIfNotExistsGeneric(
70✔
389
                t,
70✔
390

70✔
391
                //tell it how to create new instances of us related to parent
70✔
392
                this,
70✔
393

70✔
394
                //what arguments already exist
70✔
395
                GetAllArguments().ToArray())
70✔
396

70✔
397
            //convert the result back from generic to specific (us)
70✔
398
            .ToArray();
70✔
399

400
    /// <inheritdoc/>
401
    public IArgument[] CreateArgumentsForClassIfNotExists<T>() => CreateArgumentsForClassIfNotExists(typeof(T));
66✔
402

403
    /// <summary>
404
    /// Returns true if the <see cref="ProcessTaskType"/> is allowed to happen during the given <see cref="LoadStage"/>  (e.g. you can't use an IAttacher to
405
    /// load data into STAGING/LIVE - only RAW).
406
    /// </summary>
407
    /// <param name="type"></param>
408
    /// <param name="stage"></param>
409
    /// <returns></returns>
410
    public static bool IsCompatibleStage(ProcessTaskType type, LoadStage stage)
411
    {
412
        return type switch
×
413
        {
×
414
            ProcessTaskType.Executable => true,
×
415
            ProcessTaskType.SQLFile => stage != LoadStage.GetFiles,
×
416
            ProcessTaskType.SQLBakFile => stage != LoadStage.GetFiles,
×
417
            ProcessTaskType.Attacher => stage == LoadStage.Mounting,
×
418
            ProcessTaskType.DataProvider => true,
×
419
            ProcessTaskType.MutilateDataTable => stage != LoadStage.GetFiles,
×
420
            _ => throw new ArgumentOutOfRangeException(nameof(type))
×
421
        };
×
422
    }
423

424
    /// <summary>
425
    /// True if <see cref="Path"/> is the name of a C# class (as opposed to the path to an executable or SQL file etc)
426
    /// </summary>
427
    /// <returns></returns>
428
    public bool IsPluginType() => ProcessTaskType == ProcessTaskType.Attacher ||
×
429
                                  ProcessTaskType == ProcessTaskType.MutilateDataTable ||
×
430
                                  ProcessTaskType == ProcessTaskType.DataProvider;
×
431

432
    /// <summary>
433
    /// Sets the value of the corresponding <see cref="IArgument"/> (which must already exist) to the given value.  If your argument doesn't exist yet you
434
    /// can call <see cref="CreateArgumentsForClassIfNotExists"/>
435
    /// </summary>
436
    /// <param name="parameterName"></param>
437
    /// <param name="o"></param>
438
    public void SetArgumentValue(string parameterName, object o)
439
    {
440
        var matchingArgument = ProcessTaskArguments.SingleOrDefault(p => p.Name.Equals(parameterName)) ??
2,280!
441
                               throw new Exception(
114✔
442
                                   $"Could not find a ProcessTaskArgument called '{parameterName}', have you called CreateArgumentsForClassIfNotExists<T> yet?");
114✔
443
        matchingArgument.SetValue(o);
114✔
444
        matchingArgument.SaveToDatabase();
114✔
445
    }
114✔
446
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc