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

HicServices / RDMP / 21252979012

22 Jan 2026 02:53PM UTC coverage: 57.182% (-0.01%) from 57.196%
21252979012

push

github

web-flow
Task/rdmp 353 delta extractions (#2303)

* allow the addition of new columns to extraction archives

* update changelog

* allow extraction progess with no dqe set

* add option to no clone extraction progress

* update changelog

* fix typo

11497 of 21599 branches covered (53.23%)

Branch coverage included in aggregate %.

5 of 23 new or added lines in 2 files covered. (21.74%)

2 existing lines in 2 files now uncovered.

32576 of 55476 relevant lines covered (58.72%)

17872.11 hits per line

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

65.5
/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.Curation.Data;
9
using Rdmp.Core.DataExport.Data;
10
using Rdmp.Core.DataExport.DataExtraction.Commands;
11
using Rdmp.Core.DataExport.DataExtraction.UserPicks;
12
using Rdmp.Core.DataExport.DataRelease.Pipeline;
13
using Rdmp.Core.DataExport.DataRelease.Potential;
14
using Rdmp.Core.DataFlowPipeline;
15
using Rdmp.Core.DataLoad.Engine.Job.Scheduling;
16
using Rdmp.Core.DataLoad.Engine.Pipeline.Destinations;
17
using Rdmp.Core.DataLoad.Triggers.Exceptions;
18
using Rdmp.Core.DataLoad.Triggers.Implementations;
19
using Rdmp.Core.MapsDirectlyToDatabaseTable;
20
using Rdmp.Core.QueryBuilding;
21
using Rdmp.Core.Repositories;
22
using Rdmp.Core.ReusableLibraryCode;
23
using Rdmp.Core.ReusableLibraryCode.Checks;
24
using Rdmp.Core.ReusableLibraryCode.DataAccess;
25
using Rdmp.Core.ReusableLibraryCode.Progress;
26
using System;
27
using System.Data;
28
using System.Diagnostics;
29
using System.IO;
30
using System.Linq;
31
using System.Text.RegularExpressions;
32
using YamlDotNet.Core;
33

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

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

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

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

67
    [DemandsInitialization(
68
        @"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 ",
69
        defaultValue: true)]
70
    public bool DropTableIfLoadFails { get; set; }
68✔
71

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

75
    [DemandsInitialization(
76
        "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.",
77
        DefaultValue = false)]
78
    public bool CopyCollations { get; set; }
1,288✔
79

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

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

90

91
    [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.")]
92
    public bool AppendDataIfTableExists { get; set; } = false;
136✔
93

94
    [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.")]
95
    public bool IncludeTimeStamp { get; set; } = false;
102✔
96

97

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

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

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

115

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

119
    private DiscoveredDatabase _destinationDatabase;
120
    private DataTableUploadDestination _destination;
121

122
    private bool _tableDidNotExistAtStartOfLoad;
123
    private bool _isTableAlreadyNamed;
124
    private DataTable _toProcess;
125

126
    public ExecuteFullExtractionToDatabaseMSSql() : base(false)
78✔
127
    {
128
    }
78✔
129

130
    public override DataTable ProcessPipelineData(DataTable toProcess, IDataLoadEventListener job,
131
        GracefulCancellationToken cancellationToken)
132
    {
133
        _destinationDatabase = GetDestinationDatabase(job);
68✔
134
        return base.ProcessPipelineData(toProcess, job, cancellationToken);
68✔
135
    }
136

137
    protected override void Open(DataTable toProcess, IDataLoadEventListener job,
138
        GracefulCancellationToken cancellationToken)
139
    {
140
        _toProcess = toProcess;
34✔
141

142
        //give the data table the correct name
143
        if (_toProcess.ExtendedProperties.ContainsKey("ProperlyNamed") &&
34!
144
            _toProcess.ExtendedProperties["ProperlyNamed"].Equals(true))
34✔
145
            _isTableAlreadyNamed = true;
×
146

147
        _toProcess.TableName = GetTableName();
34✔
148

149
        _destination = PrepareDestination(job, _toProcess);
34✔
150
        OutputFile = _toProcess.TableName;
34✔
151
    }
34✔
152

153
    protected override void WriteRows(DataTable toProcess, IDataLoadEventListener job,
154
        GracefulCancellationToken cancellationToken, Stopwatch stopwatch)
155
    {
156
        // empty batches are allowed when using batch/resume
157
        if (toProcess.Rows.Count == 0 && _request.IsBatchResume) return;
34!
158

159
        if (_request.IsBatchResume) _destination.AllowLoadingPopulatedTables = true;
34!
160

161
        _destination.ProcessPipelineData(toProcess, job, cancellationToken);
34✔
162

163
        LinesWritten += toProcess.Rows.Count;
32✔
164
    }
32✔
165

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

173
        try
174
        {
175
            if (!_destinationDatabase.Exists())
34!
176
                _destinationDatabase.Create();
×
177

178
            if (_request is ExtractGlobalsCommand)
34!
179
                return null;
×
180

181
            var tblName = _toProcess.TableName;
34✔
182

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

199
                    listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Warning,
×
200
                        $"Table {existing.GetFullyQualifiedName()} was dropped"));
×
201

202
                    // since we dropped it we should treat it as if it was never there to begin with
203
                    _tableDidNotExistAtStartOfLoad = true;
×
204
                }
205
                else if (UseArchiveTrigger && hasPKs)
16✔
206
                {
207

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

NEW
244
                                existing = _destinationDatabase.ExpectTable(tblName);
×
NEW
245
                                implementor = triggerFactory.Create(existing);
×
NEW
246
                                present = false;
×
247
                            }
248
                        }
249
                    }
250

251
                    if (!present)
6!
252
                    {
253
                        implementor.CreateTrigger(ThrowImmediatelyCheckNotifier.Quiet);
×
254
                    }
255

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

276
        _destination = new DataTableUploadDestination(((IExtractDatasetCommand)_request).ExtractableCohort.ExternalCohortTable);
34✔
277

278
        PrimeDestinationTypesBasedOnCatalogueTypes(listener, toProcess);
34✔
279

280
        _destination.AllowResizingColumnsAtUploadTime = true;
34✔
281
        _destination.AlterTimeout = AlterTimeout;
34✔
282
        _destination.AppendDataIfTableExists = AppendDataIfTableExists;
34✔
283
        _destination.IncludeTimeStamp = IncludeTimeStamp;
34✔
284
        _destination.UseTrigger = AppendDataIfTableExists;
34✔
285
        _destination.IndexTables = IndexTables;
34✔
286
        _destination.UseTrigger = UseArchiveTrigger;
34✔
287
        _destination.IndexTableName = GetIndexName();
34✔
288
        if (UserDefinedIndex is not null)
34!
289
            _destination.UserDefinedIndexes = UserDefinedIndex.Split(',').Select(i => i.Trim()).ToList();
×
290
        _destination.PreInitialize(_destinationDatabase, listener);
34✔
291

292

293
        return _destination;
34✔
294
    }
×
295

296
    private void PrimeDestinationTypesBasedOnCatalogueTypes(IDataLoadEventListener listener, DataTable toProcess)
297
    {
298
        //if the extraction is of a Catalogue
299

300
        if (_request is not IExtractDatasetCommand datasetCommand)
34!
301
            return;
×
302

303
        //for every extractable column in the Catalogue
304
        foreach (var extractionInformation in datasetCommand.ColumnsToExtract.OfType<ExtractableColumn>()
2,512✔
305
                     .Select(ec =>
34✔
306
                         ec.CatalogueExtractionInformation))
1,256✔
307
        {
308
            if (extractionInformation == null)
1,222✔
309
                continue;
310

311
            var catItem = extractionInformation.CatalogueItem;
1,222✔
312

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

322
            if (catItem.ColumnInfo == null)
1,222!
323
            {
324
                listener.OnNotify(this,
×
325
                    new NotifyEventArgs(ProgressEventType.Warning,
×
326
                        $"Did not copy Types for ExtractionInformation {extractionInformation} (ID={extractionInformation.ID}) because it had no associated ColumnInfo"));
×
327
                continue;
×
328
            }
329

330
            if (extractionInformation.IsProperTransform())
1,222✔
331
            {
332
                listener.OnNotify(this,
2✔
333
                    new NotifyEventArgs(ProgressEventType.Warning,
2✔
334
                        $"Did not copy Types for ExtractionInformation {extractionInformation} (ID={extractionInformation.ID}) because it is a Transform"));
2✔
335
                continue;
2✔
336
            }
337

338
            var destinationType = GetDestinationDatabaseType(extractionInformation);
1,220✔
339

340
            //Tell the destination the datatype of the ColumnInfo that underlies the ExtractionInformation (this might be changed by the ExtractionInformation e.g. as a
341
            //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)
342
            //since it will just confuse the user.  Bear in mind these data types can be degraded later by the destination
343
            var columnName = extractionInformation.Alias ?? catItem.ColumnInfo.GetRuntimeName();
1,220✔
344
            var addedType = _destination.AddExplicitWriteType(columnName, destinationType);
1,220✔
345
            addedType.IsPrimaryKey = toProcess.PrimaryKey.Any(dc => dc.ColumnName == columnName);
2,436✔
346

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

351
            listener.OnNotify(this,
1,220✔
352
                new NotifyEventArgs(ProgressEventType.Information,
1,220✔
353
                    $"Set Type for {columnName} to {destinationType} (IsPrimaryKey={(addedType.IsPrimaryKey ? "true" : "false")}) to match the source table"));
1,220✔
354
        }
355

356

357
        foreach (var sub in datasetCommand.QueryBuilder.SelectColumns.Select(static sc => sc.IColumn)
1,392✔
358
                     .OfType<ReleaseIdentifierSubstitution>())
34✔
359
        {
360
            var columnName = sub.GetRuntimeName();
34✔
361
            var isPk = toProcess.PrimaryKey.Any(dc => dc.ColumnName == columnName);
62✔
362

363
            var addedType = _destination.AddExplicitWriteType(columnName,
34✔
364
                datasetCommand.ExtractableCohort.GetReleaseIdentifierDataType());
34✔
365
            addedType.IsPrimaryKey = isPk;
34✔
366
            addedType.AllowNulls = !isPk;
34✔
367
        }
368
    }
34✔
369

370
    private string GetDestinationDatabaseType(ConcreteColumn col)
371
    {
372
        //Make sure we know if we are going between database types
373
        var fromDbType = _destinationDatabase.Server.DatabaseType;
1,220✔
374
        var toDbType = col.ColumnInfo.TableInfo.DatabaseType;
1,220✔
375
        if (fromDbType != toDbType)
1,220!
376
        {
377
            var fromSyntax = col.ColumnInfo.GetQuerySyntaxHelper();
×
378
            var toSyntax = _destinationDatabase.Server.GetQuerySyntaxHelper();
×
379

380
            var intermediate = fromSyntax.TypeTranslater.GetDataTypeRequestForSQLDBType(col.ColumnInfo.Data_type);
×
381
            return toSyntax.TypeTranslater.GetSQLDBTypeForCSharpType(intermediate);
×
382
        }
383

384
        return col.ColumnInfo.Data_type;
1,220✔
385
    }
386

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

401
        if (_request is ExtractGlobalsCommand)
34!
402
        {
403
            indexName = indexName.Replace("$d", ExtractionDirectory.GLOBALS_DATA_NAME);
×
404
            indexName = indexName.Replace("$a", "G");
×
405
        }
406

407

408
        return indexName.Replace(" ", "");
34✔
409
    }
410

411
    private string GetTableName(string suffix = null)
412
    {
413
        string tblName;
414
        if (_isTableAlreadyNamed)
40!
415
        {
416
            tblName = SanitizeNameForDatabase(_toProcess.TableName);
×
417

418
            if (!string.IsNullOrWhiteSpace(suffix))
×
419
                tblName += $"_{suffix}";
×
420

421
            return tblName;
×
422
        }
423

424
        tblName = TableNamingPattern;
40✔
425
        var project = _request.Configuration.Project;
40✔
426

427
        tblName = tblName.Replace("$p", project.Name);
40✔
428
        tblName = tblName.Replace("$n", project.ProjectNumber.ToString());
40✔
429
        tblName = tblName.Replace("$c", _request.Configuration.Name);
40✔
430
        tblName = tblName.Replace("$e", _request.Configuration.ID.ToString());
40✔
431

432
        if (_request is ExtractDatasetCommand extractDatasetCommand)
40✔
433
        {
434
            tblName = tblName.Replace("$d", extractDatasetCommand.DatasetBundle.DataSet.Catalogue.Name);
38✔
435
            tblName = tblName.Replace("$a", extractDatasetCommand.DatasetBundle.DataSet.Catalogue.Acronym);
38✔
436
        }
437

438
        if (_request is ExtractGlobalsCommand)
40✔
439
        {
440
            tblName = tblName.Replace("$d", ExtractionDirectory.GLOBALS_DATA_NAME);
2✔
441
            tblName = tblName.Replace("$a", "G");
2✔
442
        }
443

444
        var cachedGetTableNameAnswer = SanitizeNameForDatabase(tblName);
40✔
445
        if (!string.IsNullOrWhiteSpace(suffix))
40✔
446
            cachedGetTableNameAnswer += $"_{suffix}";
6✔
447

448
        return cachedGetTableNameAnswer;
40✔
449
    }
450

451
    private string SanitizeNameForDatabase(string tblName)
452
    {
453
        if (_destinationDatabase == null)
40!
454
            throw new Exception(
×
455
                "Cannot pick a TableName until we know what type of server it is going to, _server is null");
×
456

457
        //get rid of brackets and dots
458
        tblName = Regex.Replace(tblName, "[.()]", "_");
40✔
459

460
        var syntax = _destinationDatabase.Server.GetQuerySyntaxHelper();
40✔
461
        syntax.ValidateTableName(tblName);
40✔
462

463
        //otherwise, fetch and cache answer
464
        var cachedGetTableNameAnswer = syntax.GetSensibleEntityNameFromString(tblName);
40✔
465

466
        return string.IsNullOrWhiteSpace(cachedGetTableNameAnswer)
40!
467
            ? throw new Exception(
40✔
468
                $"TableNamingPattern '{TableNamingPattern}' resulted in an empty string for request '{_request}'")
40✔
469
            : cachedGetTableNameAnswer;
40✔
470
    }
471

472
    public override void Dispose(IDataLoadEventListener listener, Exception pipelineFailureExceptionIfAny)
473
    {
474
        if (_destination != null)
68✔
475
        {
476
            _destination.Dispose(listener, pipelineFailureExceptionIfAny);
34✔
477

478
            //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
479
            if (pipelineFailureExceptionIfAny != null && _tableDidNotExistAtStartOfLoad && DropTableIfLoadFails)
34!
480
                if (_destinationDatabase != null)
×
481
                {
482
                    var tbl = _destinationDatabase.ExpectTable(_toProcess.TableName);
×
483

484
                    if (tbl.Exists())
×
485
                    {
486
                        listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Warning,
×
487
                            $"DropTableIfLoadFails is true so about to drop table {tbl}"));
×
488
                        tbl.Drop();
×
489
                        listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Warning, $"Dropped table {tbl}"));
×
490
                    }
491
                }
492

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

514
        TableLoadInfo?.CloseAndArchive();
68✔
515

516
        // also close off the cumulative extraction result
517
        if (_request is ExtractDatasetCommand)
68✔
518
        {
519
            var result = ((IExtractDatasetCommand)_request).CumulativeExtractionResults;
34✔
520
            if (result != null && _toProcess != null)
34✔
521
                result.CompleteAudit(GetType(), GetDestinationDescription(), TableLoadInfo.Inserts,
34✔
522
                    _request.IsBatchResume, pipelineFailureExceptionIfAny != null);
34✔
523
        }
524
    }
68✔
525

526
    public override void Abort(IDataLoadEventListener listener)
527
    {
528
        _destination?.Abort(listener);
×
529
    }
×
530

531
    protected override void PreInitializeImpl(IExtractCommand value, IDataLoadEventListener listener)
532
    {
533
    }
142✔
534

535

536
    public override string GetDestinationDescription() => GetDestinationDescription("");
66✔
537

538
    private string GetDestinationDescription(string suffix = "")
539
    {
540
        if (_toProcess == null)
100✔
541
            return _request is ExtractGlobalsCommand
34!
542
                ? "Globals"
34✔
543
                : throw new Exception("Could not describe destination because _toProcess was null");
34✔
544

545
        var tblName = _toProcess.TableName;
66✔
546
        var dbName = GetDatabaseName();
66✔
547
        return $"{TargetDatabaseServer.ID}|{dbName}|{tblName}";
66✔
548
    }
549

550
    public static DestinationType GetDestinationType() => DestinationType.Database;
×
551

552
    public override ReleasePotential GetReleasePotential(IRDMPPlatformRepositoryServiceLocator repositoryLocator,
553
        ISelectedDataSets selectedDataSet) => new MsSqlExtractionReleasePotential(repositoryLocator, selectedDataSet);
×
554

555
    public override FixedReleaseSource<ReleaseAudit> GetReleaseSource(ICatalogueRepository catalogueRepository) =>
556
        new MsSqlReleaseSource(catalogueRepository);
×
557

558
    public override GlobalReleasePotential GetGlobalReleasabilityEvaluator(
559
        IRDMPPlatformRepositoryServiceLocator repositoryLocator, ISupplementalExtractionResults globalResult,
560
        IMapsDirectlyToDatabaseTable globalToCheck) =>
561
        new MsSqlGlobalsReleasePotential(repositoryLocator, globalResult, globalToCheck);
×
562

563
    protected override void TryExtractSupportingSQLTableImpl(SupportingSQLTable sqlTable, DirectoryInfo directory,
564
        IExtractionConfiguration configuration, IDataLoadEventListener listener, out int linesWritten,
565
        out string destinationDescription)
566
    {
567
        listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Information,
4✔
568
            $"About to download SQL for global SupportingSQL {sqlTable.SQL}"));
4✔
569
        using var con = sqlTable.GetServer().GetConnection();
4✔
570
        con.Open();
4✔
571

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

575
        using var dt = new DataTable();
4✔
576
        using (var cmd = DatabaseCommandHelper.GetCommand(sqlTable.SQL, con))
4✔
577
        using (var da = DatabaseCommandHelper.GetDataAdapter(cmd))
4✔
578
        {
579
            var sw = Stopwatch.StartNew();
4✔
580
            dt.BeginLoadData();
4✔
581
            da.Fill(dt);
4✔
582
            dt.EndLoadData();
4✔
583
        }
4✔
584

585
        dt.TableName = GetTableName(_destinationDatabase.Server.GetQuerySyntaxHelper()
4✔
586
            .GetSensibleEntityNameFromString(sqlTable.Name));
4✔
587
        linesWritten = dt.Rows.Count;
4✔
588

589
        var destinationDb = GetDestinationDatabase(listener);
4✔
590
        var tbl = destinationDb.ExpectTable(dt.TableName);
4✔
591

592
        if (tbl.Exists())
4!
593
            tbl.Drop();
×
594

595
        destinationDb.CreateTable(dt.TableName, dt);
4✔
596
        destinationDescription = $"{TargetDatabaseServer.ID}|{GetDatabaseName()}|{dt.TableName}";
4✔
597
    }
8✔
598

599

600
    protected override void TryExtractLookupTableImpl(BundledLookupTable lookup, DirectoryInfo lookupDir,
601
        IExtractionConfiguration requestConfiguration, IDataLoadEventListener listener, out int linesWritten,
602
        out string destinationDescription)
603
    {
604
        using var dt = lookup.GetDataTable();
2✔
605

606
        dt.TableName = GetTableName(_destinationDatabase.Server.GetQuerySyntaxHelper()
2✔
607
            .GetSensibleEntityNameFromString(lookup.TableInfo.Name));
2✔
608

609
        //describe the destination for the abstract base
610
        destinationDescription = $"{TargetDatabaseServer.ID}|{GetDatabaseName()}|{dt.TableName}";
2✔
611
        linesWritten = dt.Rows.Count;
2✔
612

613
        var destinationDb = GetDestinationDatabase(listener);
2✔
614
        var existing = destinationDb.ExpectTable(dt.TableName);
2✔
615

616
        if (existing.Exists())
2!
617
        {
618
            listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Warning,
×
619
                $"Dropping existing Lookup table '{existing.GetFullyQualifiedName()}'"));
×
620
            existing.Drop();
×
621
        }
622

623
        destinationDb.CreateTable(dt.TableName, dt);
2✔
624
    }
4✔
625

626
    private DiscoveredDatabase GetDestinationDatabase(IDataLoadEventListener listener)
627
    {
628
        //tell user we are about to inspect it
629
        listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Information,
74✔
630
            $"About to open connection to {TargetDatabaseServer}"));
74✔
631

632
        var databaseName = GetDatabaseName();
74✔
633

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

636
        var db = discoveredServer.ExpectDatabase(databaseName);
74✔
637
        if (!db.Exists())
74✔
638
            db.Create();
10✔
639

640
        return db;
74✔
641
    }
642

643
    private string GetDatabaseName()
644
    {
645
        var dbName = DatabaseNamingPattern;
152✔
646

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

650
        if (_request == null)
152!
651
            throw new Exception("No IExtractCommand Request was passed to this component");
×
652

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

656
        dbName = dbName.Replace("$p", _project.Name)
152✔
657
            .Replace("$n", _project.ProjectNumber.ToString())
152✔
658
            .Replace("$t", _project.MasterTicket)
152✔
659
            .Replace("$r", _request.Configuration.RequestTicket)
152✔
660
            .Replace("$l", _request.Configuration.ReleaseTicket)
152✔
661
            .Replace("$e", _request.Configuration.ID.ToString());
152✔
662
        return dbName;
152✔
663
    }
664

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

676
        if (string.IsNullOrWhiteSpace(TargetDatabaseServer.Server))
8✔
677
        {
678
            notifier.OnCheckPerformed(new CheckEventArgs("TargetDatabaseServer does not have a .Server specified",
2✔
679
                CheckResult.Fail));
2✔
680
            return;
2✔
681
        }
682

683
        if (!string.IsNullOrWhiteSpace(TargetDatabaseServer.Database))
6!
684
            notifier.OnCheckPerformed(new CheckEventArgs(
×
685
                "TargetDatabaseServer has .Database specified but this will be ignored!", CheckResult.Warning));
×
686

687
        if (string.IsNullOrWhiteSpace(TableNamingPattern))
6!
688
        {
689
            notifier.OnCheckPerformed(new CheckEventArgs(
×
690
                "You must specify TableNamingPattern, this will tell the component how to name tables it generates in the remote destination",
×
691
                CheckResult.Fail));
×
692
            return;
×
693
        }
694

695
        if (string.IsNullOrWhiteSpace(DatabaseNamingPattern))
6!
696
        {
697
            notifier.OnCheckPerformed(new CheckEventArgs(
×
698
                "You must specify DatabaseNamingPattern, this will tell the component what database to create or use in the remote destination",
×
699
                CheckResult.Fail));
×
700
            return;
×
701
        }
702

703
        if (!DatabaseNamingPattern.Contains("$p") && !DatabaseNamingPattern.Contains("$n") &&
6✔
704
            !DatabaseNamingPattern.Contains("$t") && !DatabaseNamingPattern.Contains("$r") &&
6✔
705
            !DatabaseNamingPattern.Contains("$l"))
6✔
706
            notifier.OnCheckPerformed(new CheckEventArgs(
4✔
707
                "DatabaseNamingPattern does not contain any token. The tables may be created alongside existing tables and Release would be impossible.",
4✔
708
                CheckResult.Warning));
4✔
709

710
        if (!TableNamingPattern.Contains("$d") && !TableNamingPattern.Contains("$a"))
6!
711
            notifier.OnCheckPerformed(new CheckEventArgs(
×
712
                "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",
×
713
                CheckResult.Warning));
×
714

715
        if (_request == ExtractDatasetCommand.EmptyCommand)
6!
716
        {
717
            notifier.OnCheckPerformed(new CheckEventArgs(
×
718
                "Request is ExtractDatasetCommand.EmptyCommand, will not try to connect to Database",
×
719
                CheckResult.Warning));
×
720
            return;
×
721
        }
722

723
        if (TableNamingPattern != null && TableNamingPattern.Contains("$a"))
6!
724
            if (_request is ExtractDatasetCommand dsRequest && string.IsNullOrWhiteSpace(dsRequest.Catalogue.Acronym))
×
725
                notifier.OnCheckPerformed(new CheckEventArgs(
×
726
                    $"Catalogue '{dsRequest.Catalogue}' does not have an Acronym but TableNamingPattern contains $a",
×
727
                    CheckResult.Fail));
×
728

729
       
730

731
        base.Check(notifier);
6✔
732

733
        try
734
        {
735
            var server = DataAccessPortal.ExpectServer(TargetDatabaseServer, DataAccessContext.DataExport, false);
6✔
736
            var database = _destinationDatabase = server.ExpectDatabase(GetDatabaseName());
6✔
737

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

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

772
            var tables = database.DiscoverTables(false);
2✔
773

774
            if (tables.Any())
2!
775
            {
776
                string tableName;
777

778
                try
779
                {
780
                    tableName = GetTableName();
×
781
                }
×
782
                catch (Exception ex)
×
783
                {
784
                    notifier.OnCheckPerformed(
×
785
                        new CheckEventArgs("Could not determine table name", CheckResult.Fail, ex));
×
786
                    return;
×
787
                }
788

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