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

HicServices / RDMP / 26085203382

19 May 2026 08:20AM UTC coverage: 57.128% (+0.003%) from 57.125%
26085203382

push

github

web-flow
Fix security issues with dependencies (#2331)

* update deps

* add package

* bump snappier

* bump package

* add changelog entry

* bump deps

* fix license

* add license

11551 of 21753 branches covered (53.1%)

Branch coverage included in aggregate %.

32678 of 55668 relevant lines covered (58.7%)

18093.7 hits per line

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

65.68
/Rdmp.Core/DataExport/DataExtraction/Pipeline/Destinations/ExecuteFullExtractionToDatabaseMSSql.cs
1
// Copyright (c) The University of Dundee 2018-2024
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 FAnsi.Discovery;
8
using Rdmp.Core.CommandExecution;
9
using Rdmp.Core.Curation.Data;
10
using Rdmp.Core.DataExport.Data;
11
using Rdmp.Core.DataExport.DataExtraction.Commands;
12
using Rdmp.Core.DataExport.DataExtraction.UserPicks;
13
using Rdmp.Core.DataExport.DataRelease.Pipeline;
14
using Rdmp.Core.DataExport.DataRelease.Potential;
15
using Rdmp.Core.DataFlowPipeline;
16
using Rdmp.Core.DataLoad.Engine.Job.Scheduling;
17
using Rdmp.Core.DataLoad.Engine.Pipeline.Destinations;
18
using Rdmp.Core.DataLoad.Triggers.Exceptions;
19
using Rdmp.Core.DataLoad.Triggers.Implementations;
20
using Rdmp.Core.MapsDirectlyToDatabaseTable;
21
using Rdmp.Core.QueryBuilding;
22
using Rdmp.Core.Repositories;
23
using Rdmp.Core.ReusableLibraryCode;
24
using Rdmp.Core.ReusableLibraryCode.Checks;
25
using Rdmp.Core.ReusableLibraryCode.DataAccess;
26
using Rdmp.Core.ReusableLibraryCode.Progress;
27
using System;
28
using System.Data;
29
using System.Diagnostics;
30
using System.IO;
31
using System.Linq;
32
using System.Text.RegularExpressions;
33
using YamlDotNet.Core;
34

35
namespace Rdmp.Core.DataExport.DataExtraction.Pipeline.Destinations;
36

37
/// <summary>
38
/// Alternate extraction pipeline destination in which the DataTable containing the extracted dataset is written to an Sql Server database
39
/// </summary>
40
public class ExecuteFullExtractionToDatabaseMSSql : ExtractionDestination
41
{
42
    [DemandsInitialization(
43
        "External server to create the extraction into, a new database will be created for the project based on the naming pattern provided",
44
        Mandatory = true)]
45
    public IExternalDatabaseServer TargetDatabaseServer { get; set; }
370✔
46

47
    [DemandsInitialization(@"How do you want to name datasets, use the following tokens if you need them:   
48
         $p - Project Name ('e.g. My Project')
49
         $n - Project Number (e.g. 234)
50
         $t - Master Ticket (e.g. 'LINK-1234')
51
         $r - Request Ticket (e.g. 'LINK-1234')
52
         $l - Release Ticket (e.g. 'LINK-1234')
53
         $e - Extraction Configuration Id (e.g. 14)
54
         ", Mandatory = true, DefaultValue = "Proj_$n_$l")]
55
    public string DatabaseNamingPattern { get; set; }
256✔
56

57
    [DemandsInitialization(@"How do you want to name datasets, use the following tokens if you need them:   
58
         $p - Project Name ('e.g. My Project')
59
         $n - Project Number (e.g. 234)
60
         $c - Configuration Name (e.g. 'Cases')
61
         $d - Dataset name (e.g. 'Prescribing')
62
         $a - Dataset acronym (e.g. 'Presc') 
63
         $e - Extraction Configuration Id (e.g. 14)
64
         You must have either $a or $d
65
         ", Mandatory = true, DefaultValue = "$c_$d")]
66
    public string TableNamingPattern { get; set; }
138✔
67

68
    [DemandsInitialization(
69
        @"If the extraction fails half way through AND the destination table was created during the extraction then the table will be dropped from the destination rather than being left in a half loaded state ",
70
        defaultValue: true)]
71
    public bool DropTableIfLoadFails { get; set; }
68✔
72

73
    [DemandsInitialization(DataTableUploadDestination.AlterTimeout_Description, DefaultValue = 300)]
74
    public int AlterTimeout { get; set; }
102✔
75

76
    [DemandsInitialization("By applying the primary keys after writing the data, it ensures all data is extracted. Disabling this configuration may improve performance but will quickly raise issues with poorly keyed data.",DefaultValue =true)]
77
    public bool WriteDataBeforeApplyingPrimaryKeys { get; set; }
102✔
78

79
    [DemandsInitialization(
80
        "True to copy the column collations from the source database when creating the destination database.  Only works if both the source and destination have the same DatabaseType.  Excludes columns which feature a transform as part of extraction.",
81
        DefaultValue = false)]
82
    public bool CopyCollations { get; set; }
1,288✔
83

84
    [DemandsInitialization(
85
        "True to always drop the destination database table(s) from the destination if they already existed",
86
        DefaultValue = false)]
87
    public bool AlwaysDropExtractionTables { get; set; }
84✔
88

89
    [DemandsInitialization(
90
        "True to apply a distincting operation to the final table when using an ExtractionProgress.  This prevents data duplication from failed batch resumes.",
91
        DefaultValue = true)]
92
    public bool MakeFinalTableDistinctWhenBatchResuming { get; set; } = true;
146✔
93

94

95
    [DemandsInitialization("If this extraction has already been run, it will append the extraction data into the database. There is no duplication protection with this functionality.")]
96
    public bool AppendDataIfTableExists { get; set; } = false;
136✔
97

98
    [DemandsInitialization("If checked, a column names 'extraction_timestamp' will be included in the extraction that denotes the time the record was added to the extraction.")]
99
    public bool IncludeTimeStamp { get; set; } = false;
102✔
100

101

102
    [DemandsInitialization("If checked, indexed will be created using the primary keys specified")]
103
    public bool IndexTables { get; set; } = true;
180✔
104

105
    [DemandsInitialization(@"How do you want to name the created index, use the following tokens if you need them:   
106
         $p - Project Name ('e.g. My Project')
107
         $n - Project Number (e.g. 234)
108
         $c - Configuration Name (e.g. 'Cases')
109
         $d - Dataset name (e.g. 'Prescribing')
110
         $a - Dataset acronym (e.g. 'Presc') 
111
         $e - Extraction Configuration Id (e.g. 14)
112
         You must have either $a or $d
113
         ", DefaultValue = "Index_$c_$d")]
114
    public string IndexNamingPattern { get; set; }
102✔
115

116
    [DemandsInitialization("An optional list of columns to index on e.g \"Column1, Column2\"")]
117
    public string UserDefinedIndex { get; set; }
102✔
118

119

120
    [DemandsInitialization("When writing to an existing database, will update records and store historical versions via a view", DefaultValue = false)]
121
    public bool UseArchiveTrigger { get; set; }
124✔
122

123
    private DiscoveredDatabase _destinationDatabase;
124
    private DataTableUploadDestination _destination;
125

126
    private bool _tableDidNotExistAtStartOfLoad;
127
    private bool _isTableAlreadyNamed;
128
    private DataTable _toProcess;
129
    private IBasicActivateItems _activator;
130

131
    public ExecuteFullExtractionToDatabaseMSSql() : base(false)
78✔
132
    {
133
    }
78✔
134

135
    public override DataTable ProcessPipelineData(DataTable toProcess, IDataLoadEventListener job,
136
        GracefulCancellationToken cancellationToken)
137
    {
138
        _destinationDatabase = GetDestinationDatabase(job);
68✔
139
        return base.ProcessPipelineData(toProcess, job, cancellationToken);
68✔
140
    }
141

142
    protected override void Open(DataTable toProcess, IDataLoadEventListener job,
143
        GracefulCancellationToken cancellationToken)
144
    {
145
        _toProcess = toProcess;
34✔
146

147
        //give the data table the correct name
148
        if (_toProcess.ExtendedProperties.ContainsKey("ProperlyNamed") &&
34!
149
            _toProcess.ExtendedProperties["ProperlyNamed"].Equals(true))
34✔
150
            _isTableAlreadyNamed = true;
×
151

152
        _toProcess.TableName = GetTableName();
34✔
153

154
        _destination = PrepareDestination(job, _toProcess);
34✔
155
        OutputFile = _toProcess.TableName;
34✔
156
    }
34✔
157

158
    protected override void WriteRows(DataTable toProcess, IDataLoadEventListener job,
159
        GracefulCancellationToken cancellationToken, Stopwatch stopwatch)
160
    {
161
        // empty batches are allowed when using batch/resume
162
        if (toProcess.Rows.Count == 0 && _request.IsBatchResume) return;
34!
163

164
        if (_request.IsBatchResume) _destination.AllowLoadingPopulatedTables = true;
34!
165

166
        _destination.ProcessPipelineData(toProcess, job, cancellationToken);
34✔
167

168
        LinesWritten += toProcess.Rows.Count;
32✔
169
    }
32✔
170

171
    private DataTableUploadDestination PrepareDestination(IDataLoadEventListener listener, DataTable toProcess)
172
    {
173
        //see if the user has entered an extraction server/database
174
        if (TargetDatabaseServer == null)
34!
175
            throw new Exception(
×
176
                "TargetDatabaseServer (the place you want to extract the project data to) property has not been set!");
×
177

178
        try
179
        {
180
            if (!_destinationDatabase.Exists())
34!
181
                _destinationDatabase.Create();
×
182

183
            if (_request is ExtractGlobalsCommand)
34!
184
                return null;
×
185

186
            var tblName = _toProcess.TableName;
34✔
187

188
            //See if table already exists on the server (likely to cause problems including duplication, schema changes in configuration etc)
189
            var existing = _destinationDatabase.ExpectTable(tblName);
34✔
190
            if (existing.Exists())
34✔
191
            {
192
                var hasPKs = existing.DiscoverColumns().Any(col => col.IsPrimaryKey);
108✔
193
                if (_request.IsBatchResume)
16!
194
                {
195
                    listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Information,
×
196
                        $"Table {existing.GetFullyQualifiedName()} already exists but it IsBatchResume so no problem."));
×
197
                }
198
                else if (AlwaysDropExtractionTables)
16!
199
                {
200
                    listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Warning,
×
201
                        $"Table {existing.GetFullyQualifiedName()} already exists, dropping because setting {nameof(AlwaysDropExtractionTables)} is on"));
×
202
                    existing.Drop();
×
203

204
                    listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Warning,
×
205
                        $"Table {existing.GetFullyQualifiedName()} was dropped"));
×
206

207
                    // since we dropped it we should treat it as if it was never there to begin with
208
                    _tableDidNotExistAtStartOfLoad = true;
×
209
                }
210
                else if (UseArchiveTrigger && hasPKs)
16✔
211
                {
212

213
                    TriggerImplementerFactory triggerFactory = new TriggerImplementerFactory(FAnsi.DatabaseType.MicrosoftSQLServer);
6✔
214
                    var implementor = triggerFactory.Create(existing);
6✔
215
                    bool present;
216
                    try
217
                    {
218
                        present = implementor.GetTriggerStatus() == DataLoad.Triggers.TriggerStatus.Enabled;
6✔
219
                    }
6✔
220
                    catch (TriggerMissingException)
×
221
                    {
222
                        present = false;
×
223
                    }
×
224
                    //check the columns are correct, we might have added some
225
                    var existingColumns = existing.DiscoverColumns();
6✔
226
                    var existingColumnNames = existingColumns.Select(ec => ec.GetRuntimeName());
4,686✔
227
                    var toProcessColumnNames = toProcess.Columns.Cast<DataColumn>().Select(col => col.ColumnName);
240✔
228
                    var newColumns = toProcessColumnNames.Where(c => !existingColumnNames.Contains(c));
240✔
229
                    if (newColumns.Any())
6!
230
                    {
231
                        var archiveTable = _destinationDatabase.ExpectTable(tblName+"_Archive");
×
232
                        if (archiveTable.Exists())
×
233
                        {
234
                            foreach (var column in newColumns)
×
235
                            {
236
                                existing.AddColumn(column, new TypeGuesser.DatabaseTypeRequest(toProcess.Columns[column].DataType), true, 30000);
×
237
                                archiveTable.AddColumn(column, new TypeGuesser.DatabaseTypeRequest(toProcess.Columns[column].DataType), true,30000);
×
238
                            }
239
                            if (present)
×
240
                            {
241
                                string triggerProblems = "";
×
242
                                string triggerOK = "";
×
243
                                implementor.DropTrigger(out triggerProblems, out triggerOK);
×
244
                                if(triggerProblems != "")
×
245
                                {
246
                                    listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Error, triggerProblems));   
×
247
                                }
248

249
                                existing = _destinationDatabase.ExpectTable(tblName);
×
250
                                implementor = triggerFactory.Create(existing);
×
251
                                present = false;
×
252
                            }
253
                        }
254
                    }
255

256
                    if (!present)
6!
257
                    {
258
                        implementor.CreateTrigger(ThrowImmediatelyCheckNotifier.Quiet);
×
259
                    }
260

261
                }
262
                else
263
                {
264
                    listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Warning,
10✔
265
                        $"A table called {tblName} already exists on server {TargetDatabaseServer}, data load might crash if it is populated and/or has an incompatible schema"));
10✔
266
                }
267
            }
268
            else
269
            {
270
                _tableDidNotExistAtStartOfLoad = true;
18✔
271
            }
272
        }
34✔
273
        catch (Exception e)
×
274
        {
275
            //Probably the database didn't exist or the credentials were wrong or something
276
            listener.OnNotify(this,
×
277
                new NotifyEventArgs(ProgressEventType.Error,
×
278
                    "Failed to inspect destination for already existing datatables", e));
×
279
        }
×
280

281
        _destination = new DataTableUploadDestination(((IExtractDatasetCommand)_request).ExtractableCohort.ExternalCohortTable);
34✔
282

283
        PrimeDestinationTypesBasedOnCatalogueTypes(listener, toProcess);
34✔
284

285
        _destination.AllowResizingColumnsAtUploadTime = true;
34✔
286
        _destination.AlterTimeout = AlterTimeout;
34✔
287
        _destination.WriteDataBeforeApplyingPrimaryKeys = WriteDataBeforeApplyingPrimaryKeys;
34✔
288
        _destination.AppendDataIfTableExists = AppendDataIfTableExists;
34✔
289
        _destination.IncludeTimeStamp = IncludeTimeStamp;
34✔
290
        _destination.UseTrigger = AppendDataIfTableExists;
34✔
291
        _destination.IndexTables = IndexTables;
34✔
292
        _destination.UseTrigger = UseArchiveTrigger;
34✔
293
        _destination.IndexTableName = GetIndexName();
34✔
294
        if (UserDefinedIndex is not null)
34!
295
            _destination.UserDefinedIndexes = UserDefinedIndex.Split(',').Select(i => i.Trim()).ToList();
×
296
        _destination.PreInitialize(_activator,_destinationDatabase, listener);
34✔
297

298

299
        return _destination;
34✔
300
    }
×
301

302
    private void PrimeDestinationTypesBasedOnCatalogueTypes(IDataLoadEventListener listener, DataTable toProcess)
303
    {
304
        //if the extraction is of a Catalogue
305

306
        if (_request is not IExtractDatasetCommand datasetCommand)
34!
307
            return;
×
308

309
        //for every extractable column in the Catalogue
310
        foreach (var extractionInformation in datasetCommand.ColumnsToExtract.OfType<ExtractableColumn>()
2,512✔
311
                     .Select(ec =>
34✔
312
                         ec.CatalogueExtractionInformation))
1,256✔
313
        {
314
            if (extractionInformation == null)
1,222✔
315
                continue;
316

317
            var catItem = extractionInformation.CatalogueItem;
1,222✔
318

319
            //if we do not know the data type or the ei is a transform
320
            if (catItem == null)
1,222!
321
            {
322
                listener.OnNotify(this,
×
323
                    new NotifyEventArgs(ProgressEventType.Warning,
×
324
                        $"Did not copy Types for ExtractionInformation {extractionInformation} (ID={extractionInformation.ID}) because it had no associated CatalogueItem"));
×
325
                continue;
×
326
            }
327

328
            if (catItem.ColumnInfo == null)
1,222!
329
            {
330
                listener.OnNotify(this,
×
331
                    new NotifyEventArgs(ProgressEventType.Warning,
×
332
                        $"Did not copy Types for ExtractionInformation {extractionInformation} (ID={extractionInformation.ID}) because it had no associated ColumnInfo"));
×
333
                continue;
×
334
            }
335

336
            if (extractionInformation.IsProperTransform())
1,222✔
337
            {
338
                listener.OnNotify(this,
2✔
339
                    new NotifyEventArgs(ProgressEventType.Warning,
2✔
340
                        $"Did not copy Types for ExtractionInformation {extractionInformation} (ID={extractionInformation.ID}) because it is a Transform"));
2✔
341
                continue;
2✔
342
            }
343

344
            var destinationType = GetDestinationDatabaseType(extractionInformation);
1,220✔
345

346
            //Tell the destination the datatype of the ColumnInfo that underlies the ExtractionInformation (this might be changed by the ExtractionInformation e.g. as a
347
            //transform but it is a good starting point.  We don't want to create a varchar(10) column in the destination if the origin dataset (Catalogue) is a varchar(100)
348
            //since it will just confuse the user.  Bear in mind these data types can be degraded later by the destination
349
            var columnName = extractionInformation.Alias ?? catItem.ColumnInfo.GetRuntimeName();
1,220✔
350
            var addedType = _destination.AddExplicitWriteType(columnName, destinationType);
1,220✔
351
            addedType.IsPrimaryKey = toProcess.PrimaryKey.Any(dc => dc.ColumnName == columnName);
2,436✔
352

353
            //if user wants to copy collation types and the destination server is the same type as the origin server
354
            if (CopyCollations && _destinationDatabase.Server.DatabaseType == catItem.ColumnInfo.TableInfo.DatabaseType)
1,220!
355
                addedType.Collation = catItem.ColumnInfo.Collation;
×
356

357
            listener.OnNotify(this,
1,220✔
358
                new NotifyEventArgs(ProgressEventType.Information,
1,220✔
359
                    $"Set Type for {columnName} to {destinationType} (IsPrimaryKey={(addedType.IsPrimaryKey ? "true" : "false")}) to match the source table"));
1,220✔
360
        }
361

362

363
        foreach (var sub in datasetCommand.QueryBuilder.SelectColumns.Select(static sc => sc.IColumn)
1,392✔
364
                     .OfType<ReleaseIdentifierSubstitution>())
34✔
365
        {
366
            var columnName = sub.GetRuntimeName();
34✔
367
            var isPk = toProcess.PrimaryKey.Any(dc => dc.ColumnName == columnName);
62✔
368

369
            var addedType = _destination.AddExplicitWriteType(columnName,
34✔
370
                datasetCommand.ExtractableCohort.GetReleaseIdentifierDataType());
34✔
371
            addedType.IsPrimaryKey = isPk;
34✔
372
            addedType.AllowNulls = !isPk;
34✔
373
        }
374
    }
34✔
375

376
    private string GetDestinationDatabaseType(ConcreteColumn col)
377
    {
378
        //Make sure we know if we are going between database types
379
        var fromDbType = _destinationDatabase.Server.DatabaseType;
1,220✔
380
        var toDbType = col.ColumnInfo.TableInfo.DatabaseType;
1,220✔
381
        if (fromDbType != toDbType)
1,220!
382
        {
383
            var fromSyntax = col.ColumnInfo.GetQuerySyntaxHelper();
×
384
            var toSyntax = _destinationDatabase.Server.GetQuerySyntaxHelper();
×
385

386
            var intermediate = fromSyntax.TypeTranslater.GetDataTypeRequestForSQLDBType(col.ColumnInfo.Data_type);
×
387
            return toSyntax.TypeTranslater.GetSQLDBTypeForCSharpType(intermediate);
×
388
        }
389

390
        return col.ColumnInfo.Data_type;
1,220✔
391
    }
392

393
    private string GetIndexName()
394
    {
395
        string indexName = IndexNamingPattern;
34✔
396
        var project = _request.Configuration.Project;
34✔
397
        indexName = indexName.Replace("$p", project.Name);
34✔
398
        indexName = indexName.Replace("$n", project.ProjectNumber.ToString());
34✔
399
        indexName = indexName.Replace("$c", _request.Configuration.Name);
34✔
400
        indexName = indexName.Replace("$e", _request.Configuration.ID.ToString());
34✔
401
        if (_request is ExtractDatasetCommand extractDatasetCommand)
34✔
402
        {
403
            indexName = indexName.Replace("$d", extractDatasetCommand.DatasetBundle.DataSet.Catalogue.Name);
34✔
404
            indexName = indexName.Replace("$a", extractDatasetCommand.DatasetBundle.DataSet.Catalogue.Acronym);
34✔
405
        }
406

407
        if (_request is ExtractGlobalsCommand)
34!
408
        {
409
            indexName = indexName.Replace("$d", ExtractionDirectory.GLOBALS_DATA_NAME);
×
410
            indexName = indexName.Replace("$a", "G");
×
411
        }
412

413

414
        return indexName.Replace(" ", "");
34✔
415
    }
416

417
    private string GetTableName(string suffix = null)
418
    {
419
        string tblName;
420
        if (_isTableAlreadyNamed)
40!
421
        {
422
            tblName = SanitizeNameForDatabase(_toProcess.TableName);
×
423

424
            if (!string.IsNullOrWhiteSpace(suffix))
×
425
                tblName += $"_{suffix}";
×
426

427
            return tblName;
×
428
        }
429

430
        tblName = TableNamingPattern;
40✔
431
        var project = _request.Configuration.Project;
40✔
432

433
        tblName = tblName.Replace("$p", project.Name);
40✔
434
        tblName = tblName.Replace("$n", project.ProjectNumber.ToString());
40✔
435
        tblName = tblName.Replace("$c", _request.Configuration.Name);
40✔
436
        tblName = tblName.Replace("$e", _request.Configuration.ID.ToString());
40✔
437

438
        if (_request is ExtractDatasetCommand extractDatasetCommand)
40✔
439
        {
440
            tblName = tblName.Replace("$d", extractDatasetCommand.DatasetBundle.DataSet.Catalogue.Name);
38✔
441
            tblName = tblName.Replace("$a", extractDatasetCommand.DatasetBundle.DataSet.Catalogue.Acronym);
38✔
442
        }
443

444
        if (_request is ExtractGlobalsCommand)
40✔
445
        {
446
            tblName = tblName.Replace("$d", ExtractionDirectory.GLOBALS_DATA_NAME);
2✔
447
            tblName = tblName.Replace("$a", "G");
2✔
448
        }
449

450
        var cachedGetTableNameAnswer = SanitizeNameForDatabase(tblName);
40✔
451
        if (!string.IsNullOrWhiteSpace(suffix))
40✔
452
            cachedGetTableNameAnswer += $"_{suffix}";
6✔
453

454
        return cachedGetTableNameAnswer;
40✔
455
    }
456

457
    private string SanitizeNameForDatabase(string tblName)
458
    {
459
        if (_destinationDatabase == null)
40!
460
            throw new Exception(
×
461
                "Cannot pick a TableName until we know what type of server it is going to, _server is null");
×
462

463
        //get rid of brackets and dots
464
        tblName = Regex.Replace(tblName, "[.()]", "_");
40✔
465

466
        var syntax = _destinationDatabase.Server.GetQuerySyntaxHelper();
40✔
467
        syntax.ValidateTableName(tblName);
40✔
468

469
        //otherwise, fetch and cache answer
470
        var cachedGetTableNameAnswer = syntax.GetSensibleEntityNameFromString(tblName);
40✔
471

472
        return string.IsNullOrWhiteSpace(cachedGetTableNameAnswer)
40!
473
            ? throw new Exception(
40✔
474
                $"TableNamingPattern '{TableNamingPattern}' resulted in an empty string for request '{_request}'")
40✔
475
            : cachedGetTableNameAnswer;
40✔
476
    }
477

478
    public override void Dispose(IDataLoadEventListener listener, Exception pipelineFailureExceptionIfAny)
479
    {
480
        if (_destination != null)
68✔
481
        {
482
            _destination.Dispose(listener, pipelineFailureExceptionIfAny);
34✔
483

484
            //if the extraction failed, the table didn't exist in the destination (i.e. the table was created during the extraction) and we are to DropTableIfLoadFails
485
            if (pipelineFailureExceptionIfAny != null && _tableDidNotExistAtStartOfLoad && DropTableIfLoadFails)
34!
486
                if (_destinationDatabase != null)
×
487
                {
488
                    var tbl = _destinationDatabase.ExpectTable(_toProcess.TableName);
×
489

490
                    if (tbl.Exists())
×
491
                    {
492
                        listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Warning,
×
493
                            $"DropTableIfLoadFails is true so about to drop table {tbl}"));
×
494
                        tbl.Drop();
×
495
                        listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Warning, $"Dropped table {tbl}"));
×
496
                    }
497
                }
498

499
            if (pipelineFailureExceptionIfAny == null
34!
500
                && _request.IsBatchResume
34✔
501
                && MakeFinalTableDistinctWhenBatchResuming
34✔
502
                && _destinationDatabase != null
34✔
503
                && _toProcess != null)
34✔
504
            {
505
                var tbl = _destinationDatabase.ExpectTable(_toProcess.TableName);
×
506
                if (tbl.Exists())
×
507
                    // if there is no primary key then failed batches may have introduced duplication
508
                    if (!tbl.DiscoverColumns().Any(p => p.IsPrimaryKey))
×
509
                    {
510
                        listener.OnNotify(this,
×
511
                            new NotifyEventArgs(ProgressEventType.Information,
×
512
                                $"Making {tbl} distinct in case there are duplicate rows from bad batch resumes"));
×
513
                        tbl.MakeDistinct(50000000);
×
514
                        listener.OnNotify(this,
×
515
                            new NotifyEventArgs(ProgressEventType.Information, $"Finished distincting {tbl}"));
×
516
                    }
517
            }
518
        }
519

520
        TableLoadInfo?.CloseAndArchive();
68✔
521

522
        // also close off the cumulative extraction result
523
        if (_request is ExtractDatasetCommand)
68✔
524
        {
525
            var result = ((IExtractDatasetCommand)_request).CumulativeExtractionResults;
34✔
526
            if (result != null && _toProcess != null)
34✔
527
                result.CompleteAudit(GetType(), GetDestinationDescription(), TableLoadInfo.Inserts,
34✔
528
                    _request.IsBatchResume, pipelineFailureExceptionIfAny != null);
34✔
529
        }
530
    }
68✔
531

532
    public override void Abort(IDataLoadEventListener listener)
533
    {
534
        _destination?.Abort(listener);
×
535
    }
×
536

537
    protected override void PreInitializeImpl(IBasicActivateItems activator, IExtractCommand value, IDataLoadEventListener listener)
538
    {
539
        _activator = activator;
142✔
540
    }
142✔
541

542

543
    public override string GetDestinationDescription() => GetDestinationDescription("");
66✔
544

545
    private string GetDestinationDescription(string suffix = "")
546
    {
547
        if (_toProcess == null)
100✔
548
            return _request is ExtractGlobalsCommand
34!
549
                ? "Globals"
34✔
550
                : throw new Exception("Could not describe destination because _toProcess was null");
34✔
551

552
        var tblName = _toProcess.TableName;
66✔
553
        var dbName = GetDatabaseName();
66✔
554
        return $"{TargetDatabaseServer.ID}|{dbName}|{tblName}";
66✔
555
    }
556

557
    public static DestinationType GetDestinationType() => DestinationType.Database;
×
558

559
    public override ReleasePotential GetReleasePotential(IRDMPPlatformRepositoryServiceLocator repositoryLocator,
560
        ISelectedDataSets selectedDataSet) => new MsSqlExtractionReleasePotential(repositoryLocator, selectedDataSet);
×
561

562
    public override FixedReleaseSource<ReleaseAudit> GetReleaseSource(ICatalogueRepository catalogueRepository) =>
563
        new MsSqlReleaseSource(catalogueRepository);
×
564

565
    public override GlobalReleasePotential GetGlobalReleasabilityEvaluator(
566
        IRDMPPlatformRepositoryServiceLocator repositoryLocator, ISupplementalExtractionResults globalResult,
567
        IMapsDirectlyToDatabaseTable globalToCheck) =>
568
        new MsSqlGlobalsReleasePotential(repositoryLocator, globalResult, globalToCheck);
×
569

570
    protected override void TryExtractSupportingSQLTableImpl(SupportingSQLTable sqlTable, DirectoryInfo directory,
571
        IExtractionConfiguration configuration, IDataLoadEventListener listener, out int linesWritten,
572
        out string destinationDescription)
573
    {
574
        listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Information,
4✔
575
            $"About to download SQL for global SupportingSQL {sqlTable.SQL}"));
4✔
576
        using var con = sqlTable.GetServer().GetConnection();
4✔
577
        con.Open();
4✔
578

579
        listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Information,
4✔
580
            $"Connection opened successfully, about to send SQL command {sqlTable.SQL}"));
4✔
581

582
        using var dt = new DataTable();
4✔
583
        using (var cmd = DatabaseCommandHelper.GetCommand(sqlTable.SQL, con))
4✔
584
        using (var da = DatabaseCommandHelper.GetDataAdapter(cmd))
4✔
585
        {
586
            var sw = Stopwatch.StartNew();
4✔
587
            dt.BeginLoadData();
4✔
588
            da.Fill(dt);
4✔
589
            dt.EndLoadData();
4✔
590
        }
4✔
591

592
        dt.TableName = GetTableName(_destinationDatabase.Server.GetQuerySyntaxHelper()
4✔
593
            .GetSensibleEntityNameFromString(sqlTable.Name));
4✔
594
        linesWritten = dt.Rows.Count;
4✔
595

596
        var destinationDb = GetDestinationDatabase(listener);
4✔
597
        var tbl = destinationDb.ExpectTable(dt.TableName);
4✔
598

599
        if (tbl.Exists())
4!
600
            tbl.Drop();
×
601

602
        destinationDb.CreateTable(dt.TableName, dt);
4✔
603
        destinationDescription = $"{TargetDatabaseServer.ID}|{GetDatabaseName()}|{dt.TableName}";
4✔
604
    }
8✔
605

606

607
    protected override void TryExtractLookupTableImpl(BundledLookupTable lookup, DirectoryInfo lookupDir,
608
        IExtractionConfiguration requestConfiguration, IDataLoadEventListener listener, out int linesWritten,
609
        out string destinationDescription)
610
    {
611
        using var dt = lookup.GetDataTable();
2✔
612

613
        dt.TableName = GetTableName(_destinationDatabase.Server.GetQuerySyntaxHelper()
2✔
614
            .GetSensibleEntityNameFromString(lookup.TableInfo.Name));
2✔
615

616
        //describe the destination for the abstract base
617
        destinationDescription = $"{TargetDatabaseServer.ID}|{GetDatabaseName()}|{dt.TableName}";
2✔
618
        linesWritten = dt.Rows.Count;
2✔
619

620
        var destinationDb = GetDestinationDatabase(listener);
2✔
621
        var existing = destinationDb.ExpectTable(dt.TableName);
2✔
622

623
        if (existing.Exists())
2!
624
        {
625
            listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Warning,
×
626
                $"Dropping existing Lookup table '{existing.GetFullyQualifiedName()}'"));
×
627
            existing.Drop();
×
628
        }
629

630
        destinationDb.CreateTable(dt.TableName, dt);
2✔
631
    }
4✔
632

633
    private DiscoveredDatabase GetDestinationDatabase(IDataLoadEventListener listener)
634
    {
635
        //tell user we are about to inspect it
636
        listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Information,
74✔
637
            $"About to open connection to {TargetDatabaseServer}"));
74✔
638

639
        var databaseName = GetDatabaseName();
74✔
640

641
        var discoveredServer = DataAccessPortal.ExpectServer(TargetDatabaseServer, DataAccessContext.DataExport, false);
74✔
642

643
        var db = discoveredServer.ExpectDatabase(databaseName);
74✔
644
        if (!db.Exists())
74✔
645
            db.Create();
10✔
646

647
        return db;
74✔
648
    }
649

650
    private string GetDatabaseName()
651
    {
652
        var dbName = DatabaseNamingPattern;
152✔
653

654
        if (_project.ProjectNumber == null)
152!
655
            throw new ProjectNumberException($"Project '{_project}' must have a ProjectNumber");
×
656

657
        if (_request == null)
152!
658
            throw new Exception("No IExtractCommand Request was passed to this component");
×
659

660
        if (_request.Configuration == null)
152!
661
            throw new Exception($"Request did not specify any Configuration for Project '{_project}'");
×
662

663
        dbName = dbName.Replace("$p", _project.Name)
152✔
664
            .Replace("$n", _project.ProjectNumber.ToString())
152✔
665
            .Replace("$t", _project.MasterTicket)
152✔
666
            .Replace("$r", _request.Configuration.RequestTicket)
152✔
667
            .Replace("$l", _request.Configuration.ReleaseTicket)
152✔
668
            .Replace("$e", _request.Configuration.ID.ToString());
152✔
669
        return dbName;
152✔
670
    }
671

672
    public override void Check(ICheckNotifier notifier)
673
    {
674
        if (TargetDatabaseServer == null)
10✔
675
        {
676
            notifier.OnCheckPerformed(new CheckEventArgs(
2✔
677
                "Target database server property has not been set (This component does not know where to extract data to!), " +
2✔
678
                "to fix this you must edit the pipeline and choose an ExternalDatabaseServer to extract to)",
2✔
679
                CheckResult.Fail));
2✔
680
            return;
2✔
681
        }
682

683
        if (string.IsNullOrWhiteSpace(TargetDatabaseServer.Server))
8✔
684
        {
685
            notifier.OnCheckPerformed(new CheckEventArgs("TargetDatabaseServer does not have a .Server specified",
2✔
686
                CheckResult.Fail));
2✔
687
            return;
2✔
688
        }
689

690
        if (!string.IsNullOrWhiteSpace(TargetDatabaseServer.Database))
6!
691
            notifier.OnCheckPerformed(new CheckEventArgs(
×
692
                "TargetDatabaseServer has .Database specified but this will be ignored!", CheckResult.Warning));
×
693

694
        if (string.IsNullOrWhiteSpace(TableNamingPattern))
6!
695
        {
696
            notifier.OnCheckPerformed(new CheckEventArgs(
×
697
                "You must specify TableNamingPattern, this will tell the component how to name tables it generates in the remote destination",
×
698
                CheckResult.Fail));
×
699
            return;
×
700
        }
701

702
        if (string.IsNullOrWhiteSpace(DatabaseNamingPattern))
6!
703
        {
704
            notifier.OnCheckPerformed(new CheckEventArgs(
×
705
                "You must specify DatabaseNamingPattern, this will tell the component what database to create or use in the remote destination",
×
706
                CheckResult.Fail));
×
707
            return;
×
708
        }
709

710
        if (!DatabaseNamingPattern.Contains("$p") && !DatabaseNamingPattern.Contains("$n") &&
6✔
711
            !DatabaseNamingPattern.Contains("$t") && !DatabaseNamingPattern.Contains("$r") &&
6✔
712
            !DatabaseNamingPattern.Contains("$l"))
6✔
713
            notifier.OnCheckPerformed(new CheckEventArgs(
4✔
714
                "DatabaseNamingPattern does not contain any token. The tables may be created alongside existing tables and Release would be impossible.",
4✔
715
                CheckResult.Warning));
4✔
716

717
        if (!TableNamingPattern.Contains("$d") && !TableNamingPattern.Contains("$a"))
6!
718
            notifier.OnCheckPerformed(new CheckEventArgs(
×
719
                "TableNamingPattern must contain either $d or $a, the name/acronym of the dataset being extracted otherwise you will get collisions when you extract multiple tables at once",
×
720
                CheckResult.Warning));
×
721

722
        if (_request == ExtractDatasetCommand.EmptyCommand)
6!
723
        {
724
            notifier.OnCheckPerformed(new CheckEventArgs(
×
725
                "Request is ExtractDatasetCommand.EmptyCommand, will not try to connect to Database",
×
726
                CheckResult.Warning));
×
727
            return;
×
728
        }
729

730
        if (TableNamingPattern != null && TableNamingPattern.Contains("$a"))
6!
731
            if (_request is ExtractDatasetCommand dsRequest && string.IsNullOrWhiteSpace(dsRequest.Catalogue.Acronym))
×
732
                notifier.OnCheckPerformed(new CheckEventArgs(
×
733
                    $"Catalogue '{dsRequest.Catalogue}' does not have an Acronym but TableNamingPattern contains $a",
×
734
                    CheckResult.Fail));
×
735

736
       
737

738
        base.Check(notifier);
6✔
739

740
        try
741
        {
742
            var server = DataAccessPortal.ExpectServer(TargetDatabaseServer, DataAccessContext.DataExport, false);
6✔
743
            var database = _destinationDatabase = server.ExpectDatabase(GetDatabaseName());
6✔
744

745
            if (UseArchiveTrigger)
6!
746
            {
747
                if (_request is ExtractDatasetCommand dsRequest)
×
748
                {
749
                    var existing = _destinationDatabase.ExpectTable(dsRequest.Catalogue.Name);
×
750
                    if (existing.Exists())
×
751
                    {
752
                        var hasPKs = existing.DiscoverColumns().Any(col => col.IsPrimaryKey);
×
753
                        if (!hasPKs)
×
754
                        {
755
                            notifier.OnCheckPerformed(new CheckEventArgs(
×
756
                               $"Catalogue does not have any PKS. Cannot apply the archive trigger",
×
757
                               CheckResult.Fail));
×
758
                        }
759
                    }
760
                }
761
            }
762

763
            if (database.Exists())
6✔
764
            {
765
                notifier.OnCheckPerformed(
2✔
766
                    new CheckEventArgs(
2✔
767
                        $"Database {database} already exists! if an extraction has already been run you may have errors if you are re-extracting the same tables",
2✔
768
                        CheckResult.Warning));
2✔
769
            }
770
            else
771
            {
772
                notifier.OnCheckPerformed(
4✔
773
                    new CheckEventArgs(
4✔
774
                        $"Database {database} does not exist on server... it will be created at runtime",
4✔
775
                        CheckResult.Success));
4✔
776
                return;
4✔
777
            }
778

779
            var tables = database.DiscoverTables(false);
2✔
780

781
            if (tables.Any())
2!
782
            {
783
                string tableName;
784

785
                try
786
                {
787
                    tableName = GetTableName();
×
788
                }
×
789
                catch (Exception ex)
×
790
                {
791
                    notifier.OnCheckPerformed(
×
792
                        new CheckEventArgs("Could not determine table name", CheckResult.Fail, ex));
×
793
                    return;
×
794
                }
795

796
                // if the expected table exists and we are not doing a batch resume or allowing data appending
797
                if (tables.Any(t => t.GetRuntimeName().Equals(tableName)) && !_request.IsBatchResume && !AppendDataIfTableExists)
×
798
                    notifier.OnCheckPerformed(new CheckEventArgs(ErrorCodes.ExistingExtractionTableInDatabase,
×
799
                        tableName, database));
×
800
            }
801
            else
802
            {
803
                notifier.OnCheckPerformed(new CheckEventArgs($"Confirmed that database {database} is empty of tables",
2✔
804
                    CheckResult.Success));
2✔
805
            }
806
        }
2✔
807
        catch (Exception e)
×
808
        {
809
            notifier.OnCheckPerformed(new CheckEventArgs(
×
810
                $"Could not connect to TargetDatabaseServer '{TargetDatabaseServer}'", CheckResult.Fail, e));
×
811
        }
×
812
    }
6✔
813
}
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

© 2026 Coveralls, Inc