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

HicServices / RDMP / 28572360334

02 Jul 2026 07:13AM UTC coverage: 56.995% (+0.02%) from 56.975%
28572360334

Pull #2343

github

JFriel
update merge component
Pull Request #2343: Task/rdmp 376 extraction structural changes

11608 of 21899 branches covered (53.01%)

Branch coverage included in aggregate %.

54 of 96 new or added lines in 4 files covered. (56.25%)

9 existing lines in 2 files now uncovered.

32771 of 55966 relevant lines covered (58.56%)

9432.03 hits per line

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

70.13
/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.Pipeline.Destinations;
17
using Rdmp.Core.DataLoad.Triggers;
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

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; }
347✔
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; }
236✔
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; }
123✔
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; }
70✔
71

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

75
    [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)]
76
    public bool WriteDataBeforeApplyingPrimaryKeys { get; set; }
105✔
77

78
    [DemandsInitialization(
79
        "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.",
80
        DefaultValue = false)]
81
    public bool CopyCollations { get; set; }
1,352✔
82

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

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

93

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

97
    [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.")]
98
    public bool IncludeTimeStamp { get; set; } = false;
105✔
99

100

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

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

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

118

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

122
    private DiscoveredDatabase _destinationDatabase;
123
    private DataTableUploadDestination _destination;
124

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

130
    public ExecuteFullExtractionToDatabaseMSSql() : base(false)
75✔
131
    {
132
    }
75✔
133

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

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

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

151
        _toProcess.TableName = GetTableName();
35✔
152

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

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

163
        if (_request.IsBatchResume) _destination.AllowLoadingPopulatedTables = true;
35!
164

165
        _destination.ProcessPipelineData(toProcess, job, cancellationToken);
35✔
166

167
        LinesWritten += toProcess.Rows.Count;
34✔
168
    }
34✔
169

170

171
    private bool hasStructuralChanges(DataTable source, DiscoveredTable destination)
172
    {
173
        var sourceColumns = source.Columns.Cast<DataColumn>().Select(c => c.ColumnName).ToList();
753✔
174
        var destinationColumns = destination.DiscoverColumns().Select(c => c.GetRuntimeName()).ToList();
766✔
175
        return !sourceColumns.All(destinationColumns.Contains) || !destinationColumns.All(sourceColumns.Contains);
19✔
176
    }
177

178

179
    private DataTableUploadDestination PrepareDestination(IDataLoadEventListener listener, DataTable toProcess)
180
    {
181
        //see if the user has entered an extraction server/database
182
        if (TargetDatabaseServer == null)
35!
183
            throw new Exception(
×
184
                "TargetDatabaseServer (the place you want to extract the project data to) property has not been set!");
×
185

186
        try
187
        {
188
            if (!_destinationDatabase.Exists())
35!
189
                _destinationDatabase.Create();
×
190

191
            if (_request is ExtractGlobalsCommand)
35!
192
                return null;
×
193

194
            var tblName = _toProcess.TableName;
35✔
195

196
            //See if table already exists on the server (likely to cause problems including duplication, schema changes in configuration etc)
197
            var existing = _destinationDatabase.ExpectTable(tblName);
35✔
198
            if (existing.Exists())
35✔
199
            {
200
                var hasPKs = existing.DiscoverColumns().Any(col => col.IsPrimaryKey);
76✔
201
                TriggerImplementerFactory triggerFactory = new TriggerImplementerFactory(FAnsi.DatabaseType.MicrosoftSQLServer);
19✔
202
                var implementor = triggerFactory.Create(existing);
19✔
203
                bool triggerPresent;
204
                try
205
                {
206
                    triggerPresent = implementor.GetTriggerStatus() == DataLoad.Triggers.TriggerStatus.Enabled;
19✔
207
                }
19✔
NEW
208
                catch (TriggerMissingException)
×
209
                {
NEW
210
                    triggerPresent = false;
×
NEW
211
                }
×
212
                if (!AlwaysDropExtractionTables)
19✔
213
                {
214
                    //check the PKs are the same
215
                    var remotePKs = existing.DiscoverColumns().Where(col => col.IsPrimaryKey).Select(col => col.GetRuntimeName()).ToList();
785✔
216
                    var rdmpPKs = toProcess.PrimaryKey.Cast<DataColumn>().Select(col => col.ColumnName).ToList();
38✔
217
                    if (!remotePKs.All(rdmpPKs.Contains) || remotePKs.Count != rdmpPKs.Count)
19!
218
                    {
219
                        listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Error,
×
220
                            $"""
×
221
                        Table {existing.GetFullyQualifiedName()} already exists and has different PKs to the source table.                            
×
222
                        Source PKs: {string.Join(", ", rdmpPKs)}
×
223
                        Destination PKs: {string.Join(", ", remotePKs)}
×
224
                        """));
×
225
                        return null;
×
226
                    }
227
                    if (hasStructuralChanges(toProcess, existing))
19✔
228
                    {
229
                        var sourceColumns = toProcess.Columns.Cast<DataColumn>().Select(c => c.ColumnName).ToList();
553✔
230
                        var destinationColumns = existing.DiscoverColumns().Select(c => c.GetRuntimeName()).ToList();
566✔
231

232
                        if (triggerPresent && destinationColumns.Except(sourceColumns).Where(c => !SpecialFieldNames.IsHicPrefixed(c)).Any())//only mess about with column removal if there is an archive trigger
29✔
233
                        {
234

235
                            //move everything into the archive - do this by updating the HIC_validfrom
236
                            var sql = $"UPDATE {existing.GetFullyQualifiedName()} set {SpecialFieldNames.ValidFrom} = GETDATE()";
5✔
237
                            using var con = _destinationDatabase.Server.GetConnection();
5✔
238
                            con.Open();
5✔
239
                            using var cmd = _destinationDatabase.Server.GetCommand(sql, con);
5✔
240
                            cmd.CommandTimeout = 30000;
5✔
241
                            cmd.ExecuteNonQuery();
5✔
242

243
                            var removedColumns = destinationColumns.Except(sourceColumns).Where(c => !SpecialFieldNames.IsHicPrefixed(c));
15✔
244
                            foreach (var column in removedColumns.Select(c => existing.DiscoverColumn(c)))
25✔
245
                            {
246
                                existing.DropColumn(column);
5✔
247
                            }
248
                            string triggerProblems = "";
5✔
249
                            string triggerOK = "";
5✔
250
                            implementor.DropTrigger(out triggerProblems, out triggerOK);
5✔
251
                            if (triggerProblems != "")
5!
252
                            {
NEW
253
                                listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Error, triggerProblems));
×
254
                            }
255

256
                            existing = _destinationDatabase.ExpectTable(tblName);
5✔
257
                            implementor = triggerFactory.Create(existing);
5✔
258
                            try
259
                            {
260
                                triggerPresent = implementor.GetTriggerStatus() == DataLoad.Triggers.TriggerStatus.Enabled;
5✔
261
                            }
5✔
NEW
262
                            catch (TriggerMissingException)
×
263
                            {
NEW
264
                                triggerPresent = false;
×
NEW
265
                            }
×
266
                        }
267
                    }
268
                }
269

270
                if (_request.IsBatchResume)
19!
271
                {
272
                    listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Information,
×
273
                        $"Table {existing.GetFullyQualifiedName()} already exists but it IsBatchResume so no problem."));
×
274
                }
275
                else if (AlwaysDropExtractionTables)
19!
276
                {
277
                    listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Warning,
×
278
                        $"Table {existing.GetFullyQualifiedName()} already exists, dropping because setting {nameof(AlwaysDropExtractionTables)} is on"));
×
279
                    existing.Drop();
×
280

281
                    listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Warning,
×
282
                        $"Table {existing.GetFullyQualifiedName()} was dropped"));
×
283

284
                    // since we dropped it we should treat it as if it was never there to begin with
285
                    _tableDidNotExistAtStartOfLoad = true;
×
286
                }
287
                else if (UseArchiveTrigger && hasPKs)
19✔
288
                {
289
                    //check the columns are correct, we might have added some
290
                    var existingColumns = existing.DiscoverColumns();
14✔
291
                    var existingColumnNames = existingColumns.Select(ec => ec.GetRuntimeName());
10,989✔
292
                    var toProcessColumnNames = toProcess.Columns.Cast<DataColumn>().Select(col => col.ColumnName);
581✔
293
                    var newColumns = toProcessColumnNames.Where(c => !existingColumnNames.Contains(c));
581✔
294
                    if (newColumns.Any())
14✔
295
                    {
296
                        var archiveTable = _destinationDatabase.ExpectTable(tblName + "_Archive");
6✔
297
                        if (archiveTable.Exists())
6✔
298
                        {
299
                            foreach (var column in newColumns)
24✔
300
                            {
301
                                existing.AddColumn(column, new TypeGuesser.DatabaseTypeRequest(toProcess.Columns[column].DataType), true, 30000);
6✔
302
                                if (archiveTable.DiscoverColumns().All(col => col.GetRuntimeName() != column))
219✔
303
                                {
304
                                    archiveTable.AddColumn(column, new TypeGuesser.DatabaseTypeRequest(toProcess.Columns[column].DataType), true, 30000);
5✔
305
                                }
306
                            }
307
                            if (triggerPresent)
6✔
308
                            {
309
                                string triggerProblems = "";
6✔
310
                                string triggerOK = "";
6✔
311
                                implementor.DropTrigger(out triggerProblems, out triggerOK);
6✔
312
                                if (triggerProblems != "")
6!
313
                                {
314
                                    listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Error, triggerProblems));
×
315
                                }
316

317
                                existing = _destinationDatabase.ExpectTable(tblName);
6✔
318
                                implementor = triggerFactory.Create(existing);
6✔
319
                                triggerPresent = false;
6✔
320
                            }
321
                        }
322
                    }
323

324
                    if (!triggerPresent)
14✔
325
                    {
326
                        implementor.CreateTrigger(ThrowImmediatelyCheckNotifier.Quiet);
11✔
327
                    }
328

329
                }
330
                else
331
                {
332
                    listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Warning,
5✔
333
                        $"A table called {tblName} already exists on server {TargetDatabaseServer}, data load might crash if it is populated and/or has an incompatible schema"));
5✔
334
                }
335
            }
336
            else
337
            {
338
                _tableDidNotExistAtStartOfLoad = true;
16✔
339
            }
340
        }
35✔
341
        catch (Exception e)
×
342
        {
343
            //Probably the database didn't exist or the credentials were wrong or something
344
            listener.OnNotify(this,
×
345
                new NotifyEventArgs(ProgressEventType.Error,
×
346
                    "Failed to inspect destination for already existing datatables", e));
×
347
        }
×
348

349
        _destination = new DataTableUploadDestination(((IExtractDatasetCommand)_request).ExtractableCohort.ExternalCohortTable);
35✔
350

351
        PrimeDestinationTypesBasedOnCatalogueTypes(listener, toProcess);
35✔
352

353
        _destination.AllowResizingColumnsAtUploadTime = true;
35✔
354
        _destination.AlterTimeout = AlterTimeout;
35✔
355
        _destination.WriteDataBeforeApplyingPrimaryKeys = WriteDataBeforeApplyingPrimaryKeys;
35✔
356
        _destination.AppendDataIfTableExists = AppendDataIfTableExists;
35✔
357
        _destination.IncludeTimeStamp = IncludeTimeStamp;
35✔
358
        _destination.UseTrigger = AppendDataIfTableExists;
35✔
359
        _destination.IndexTables = IndexTables;
35✔
360
        _destination.UseTrigger = UseArchiveTrigger;
35✔
361
        _destination.IndexTableName = GetIndexName();
35✔
362
        if (UserDefinedIndex is not null)
35!
363
            _destination.UserDefinedIndexes = UserDefinedIndex.Split(',').Select(i => i.Trim()).ToList();
×
364
        _destination.PreInitialize(_activator, _destinationDatabase, listener);
35✔
365

366

367
        return _destination;
35✔
368
    }
×
369

370
    private void PrimeDestinationTypesBasedOnCatalogueTypes(IDataLoadEventListener listener, DataTable toProcess)
371
    {
372
        //if the extraction is of a Catalogue
373

374
        if (_request is not IExtractDatasetCommand datasetCommand)
35!
375
            return;
×
376

377
        //for every extractable column in the Catalogue
378
        foreach (var extractionInformation in datasetCommand.ColumnsToExtract.OfType<ExtractableColumn>()
2,636✔
379
                     .Select(ec =>
35✔
380
                         ec.CatalogueExtractionInformation))
1,318✔
381
        {
382
            if (extractionInformation == null)
1,283✔
383
                continue;
384

385
            var catItem = extractionInformation.CatalogueItem;
1,283✔
386

387
            //if we do not know the data type or the ei is a transform
388
            if (catItem == null)
1,283!
389
            {
390
                listener.OnNotify(this,
×
391
                    new NotifyEventArgs(ProgressEventType.Warning,
×
392
                        $"Did not copy Types for ExtractionInformation {extractionInformation} (ID={extractionInformation.ID}) because it had no associated CatalogueItem"));
×
393
                continue;
×
394
            }
395

396
            if (catItem.ColumnInfo == null)
1,283!
397
            {
398
                listener.OnNotify(this,
×
399
                    new NotifyEventArgs(ProgressEventType.Warning,
×
400
                        $"Did not copy Types for ExtractionInformation {extractionInformation} (ID={extractionInformation.ID}) because it had no associated ColumnInfo"));
×
401
                continue;
×
402
            }
403

404
            if (extractionInformation.IsProperTransform())
1,283✔
405
            {
406
                listener.OnNotify(this,
1✔
407
                    new NotifyEventArgs(ProgressEventType.Warning,
1✔
408
                        $"Did not copy Types for ExtractionInformation {extractionInformation} (ID={extractionInformation.ID}) because it is a Transform"));
1✔
409
                continue;
1✔
410
            }
411

412
            var destinationType = GetDestinationDatabaseType(extractionInformation);
1,282✔
413

414
            //Tell the destination the datatype of the ColumnInfo that underlies the ExtractionInformation (this might be changed by the ExtractionInformation e.g. as a
415
            //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)
416
            //since it will just confuse the user.  Bear in mind these data types can be degraded later by the destination
417
            var columnName = extractionInformation.Alias ?? catItem.ColumnInfo.GetRuntimeName();
1,282✔
418
            var addedType = _destination.AddExplicitWriteType(columnName, destinationType);
1,282✔
419
            addedType.IsPrimaryKey = toProcess.PrimaryKey.Any(dc => dc.ColumnName == columnName);
2,562✔
420

421
            //if user wants to copy collation types and the destination server is the same type as the origin server
422
            if (CopyCollations && _destinationDatabase.Server.DatabaseType == catItem.ColumnInfo.TableInfo.DatabaseType)
1,282!
423
                addedType.Collation = catItem.ColumnInfo.Collation;
×
424

425
            listener.OnNotify(this,
1,282✔
426
                new NotifyEventArgs(ProgressEventType.Information,
1,282✔
427
                    $"Set Type for {columnName} to {destinationType} (IsPrimaryKey={(addedType.IsPrimaryKey ? "true" : "false")}) to match the source table"));
1,282✔
428
        }
429

430

431
        foreach (var sub in datasetCommand.QueryBuilder.SelectColumns.Select(static sc => sc.IColumn)
1,458✔
432
                     .OfType<ReleaseIdentifierSubstitution>())
35✔
433
        {
434
            var columnName = sub.GetRuntimeName();
35✔
435
            var isPk = toProcess.PrimaryKey.Any(dc => dc.ColumnName == columnName);
67✔
436

437
            var addedType = _destination.AddExplicitWriteType(columnName,
35✔
438
                datasetCommand.ExtractableCohort.GetReleaseIdentifierDataType());
35✔
439
            addedType.IsPrimaryKey = isPk;
35✔
440
            addedType.AllowNulls = !isPk;
35✔
441
        }
442
    }
35✔
443

444
    private string GetDestinationDatabaseType(ConcreteColumn col)
445
    {
446
        //Make sure we know if we are going between database types
447
        var fromDbType = _destinationDatabase.Server.DatabaseType;
1,282✔
448
        var toDbType = col.ColumnInfo.TableInfo.DatabaseType;
1,282✔
449
        if (fromDbType != toDbType)
1,282!
450
        {
451
            var fromSyntax = col.ColumnInfo.GetQuerySyntaxHelper();
×
452
            var toSyntax = _destinationDatabase.Server.GetQuerySyntaxHelper();
×
453

454
            var intermediate = fromSyntax.TypeTranslater.GetDataTypeRequestForSQLDBType(col.ColumnInfo.Data_type);
×
455
            return toSyntax.TypeTranslater.GetSQLDBTypeForCSharpType(intermediate);
×
456
        }
457

458
        return col.ColumnInfo.Data_type;
1,282✔
459
    }
460

461
    private string GetIndexName()
462
    {
463
        string indexName = IndexNamingPattern;
35✔
464
        var project = _request.Configuration.Project;
35✔
465
        indexName = indexName.Replace("$p", project.Name);
35✔
466
        indexName = indexName.Replace("$n", project.ProjectNumber.ToString());
35✔
467
        indexName = indexName.Replace("$c", _request.Configuration.Name);
35✔
468
        indexName = indexName.Replace("$e", _request.Configuration.ID.ToString());
35✔
469
        if (_request is ExtractDatasetCommand extractDatasetCommand)
35✔
470
        {
471
            indexName = indexName.Replace("$d", extractDatasetCommand.DatasetBundle.DataSet.Catalogue.Name);
35✔
472
            indexName = indexName.Replace("$a", extractDatasetCommand.DatasetBundle.DataSet.Catalogue.Acronym);
35✔
473
        }
474

475
        if (_request is ExtractGlobalsCommand)
35!
476
        {
477
            indexName = indexName.Replace("$d", ExtractionDirectory.GLOBALS_DATA_NAME);
×
478
            indexName = indexName.Replace("$a", "G");
×
479
        }
480

481

482
        return indexName.Replace(" ", "");
35✔
483
    }
484

485
    private string GetTableName(string suffix = null)
486
    {
487
        string tblName;
488
        if (_isTableAlreadyNamed)
38!
489
        {
490
            tblName = SanitizeNameForDatabase(_toProcess.TableName);
×
491

492
            if (!string.IsNullOrWhiteSpace(suffix))
×
493
                tblName += $"_{suffix}";
×
494

495
            return tblName;
×
496
        }
497

498
        tblName = TableNamingPattern;
38✔
499
        var project = _request.Configuration.Project;
38✔
500

501
        tblName = tblName.Replace("$p", project.Name);
38✔
502
        tblName = tblName.Replace("$n", project.ProjectNumber.ToString());
38✔
503
        tblName = tblName.Replace("$c", _request.Configuration.Name);
38✔
504
        tblName = tblName.Replace("$e", _request.Configuration.ID.ToString());
38✔
505

506
        if (_request is ExtractDatasetCommand extractDatasetCommand)
38✔
507
        {
508
            tblName = tblName.Replace("$d", extractDatasetCommand.DatasetBundle.DataSet.Catalogue.Name);
37✔
509
            tblName = tblName.Replace("$a", extractDatasetCommand.DatasetBundle.DataSet.Catalogue.Acronym);
37✔
510
        }
511

512
        if (_request is ExtractGlobalsCommand)
38✔
513
        {
514
            tblName = tblName.Replace("$d", ExtractionDirectory.GLOBALS_DATA_NAME);
1✔
515
            tblName = tblName.Replace("$a", "G");
1✔
516
        }
517

518
        var cachedGetTableNameAnswer = SanitizeNameForDatabase(tblName);
38✔
519
        if (!string.IsNullOrWhiteSpace(suffix))
38✔
520
            cachedGetTableNameAnswer += $"_{suffix}";
3✔
521

522
        return cachedGetTableNameAnswer;
38✔
523
    }
524

525
    private string SanitizeNameForDatabase(string tblName)
526
    {
527
        if (_destinationDatabase == null)
38!
528
            throw new Exception(
×
529
                "Cannot pick a TableName until we know what type of server it is going to, _server is null");
×
530

531
        //get rid of brackets and dots
532
        tblName = Regex.Replace(tblName, "[.()]", "_");
38✔
533

534
        var syntax = _destinationDatabase.Server.GetQuerySyntaxHelper();
38✔
535
        syntax.ValidateTableName(tblName);
38✔
536

537
        //otherwise, fetch and cache answer
538
        var cachedGetTableNameAnswer = syntax.GetSensibleEntityNameFromString(tblName);
38✔
539

540
        return string.IsNullOrWhiteSpace(cachedGetTableNameAnswer)
38!
541
            ? throw new Exception(
38✔
542
                $"TableNamingPattern '{TableNamingPattern}' resulted in an empty string for request '{_request}'")
38✔
543
            : cachedGetTableNameAnswer;
38✔
544
    }
545

546
    public override void Dispose(IDataLoadEventListener listener, Exception pipelineFailureExceptionIfAny)
547
    {
548
        if (_destination != null)
70✔
549
        {
550
            _destination.Dispose(listener, pipelineFailureExceptionIfAny);
35✔
551

552
            //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
553
            if (pipelineFailureExceptionIfAny != null && _tableDidNotExistAtStartOfLoad && DropTableIfLoadFails)
35!
554
                if (_destinationDatabase != null)
×
555
                {
556
                    var tbl = _destinationDatabase.ExpectTable(_toProcess.TableName);
×
557

558
                    if (tbl.Exists())
×
559
                    {
560
                        listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Warning,
×
561
                            $"DropTableIfLoadFails is true so about to drop table {tbl}"));
×
562
                        tbl.Drop();
×
563
                        listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Warning, $"Dropped table {tbl}"));
×
564
                    }
565
                }
566

567
            if (pipelineFailureExceptionIfAny == null
35!
568
                && _request.IsBatchResume
35✔
569
                && MakeFinalTableDistinctWhenBatchResuming
35✔
570
                && _destinationDatabase != null
35✔
571
                && _toProcess != null)
35✔
572
            {
573
                var tbl = _destinationDatabase.ExpectTable(_toProcess.TableName);
×
574
                if (tbl.Exists())
×
575
                    // if there is no primary key then failed batches may have introduced duplication
576
                    if (!tbl.DiscoverColumns().Any(p => p.IsPrimaryKey))
×
577
                    {
578
                        listener.OnNotify(this,
×
579
                            new NotifyEventArgs(ProgressEventType.Information,
×
580
                                $"Making {tbl} distinct in case there are duplicate rows from bad batch resumes"));
×
581
                        tbl.MakeDistinct(50000000);
×
582
                        listener.OnNotify(this,
×
583
                            new NotifyEventArgs(ProgressEventType.Information, $"Finished distincting {tbl}"));
×
584
                    }
585
            }
586
        }
587

588
        TableLoadInfo?.CloseAndArchive();
70✔
589

590
        // also close off the cumulative extraction result
591
        if (_request is ExtractDatasetCommand)
70✔
592
        {
593
            var result = ((IExtractDatasetCommand)_request).CumulativeExtractionResults;
35✔
594
            if (result != null && _toProcess != null)
35✔
595
                result.CompleteAudit(GetType(), GetDestinationDescription(), TableLoadInfo.Inserts,
35✔
596
                    _request.IsBatchResume, pipelineFailureExceptionIfAny != null);
35✔
597
        }
598
    }
70✔
599

600
    public override void Abort(IDataLoadEventListener listener)
601
    {
602
        _destination?.Abort(listener);
×
603
    }
×
604

605
    protected override void PreInitializeImpl(IBasicActivateItems activator, IExtractCommand value, IDataLoadEventListener listener)
606
    {
607
        _activator = activator;
143✔
608
    }
143✔
609

610

611
    public override string GetDestinationDescription() => GetDestinationDescription("");
69✔
612

613
    private string GetDestinationDescription(string suffix = "")
614
    {
615
        if (_toProcess == null)
104✔
616
            return _request is ExtractGlobalsCommand
35!
617
                ? "Globals"
35✔
618
                : throw new Exception("Could not describe destination because _toProcess was null");
35✔
619

620
        var tblName = _toProcess.TableName;
69✔
621
        var dbName = GetDatabaseName();
69✔
622
        return $"{TargetDatabaseServer.ID}|{dbName}|{tblName}";
69✔
623
    }
624

625
    public static DestinationType GetDestinationType() => DestinationType.Database;
×
626

627
    public override ReleasePotential GetReleasePotential(IRDMPPlatformRepositoryServiceLocator repositoryLocator,
628
        ISelectedDataSets selectedDataSet) => new MsSqlExtractionReleasePotential(repositoryLocator, selectedDataSet);
×
629

630
    public override FixedReleaseSource<ReleaseAudit> GetReleaseSource(ICatalogueRepository catalogueRepository) =>
631
        new MsSqlReleaseSource(catalogueRepository);
×
632

633
    public override GlobalReleasePotential GetGlobalReleasabilityEvaluator(
634
        IRDMPPlatformRepositoryServiceLocator repositoryLocator, ISupplementalExtractionResults globalResult,
635
        IMapsDirectlyToDatabaseTable globalToCheck) =>
636
        new MsSqlGlobalsReleasePotential(repositoryLocator, globalResult, globalToCheck);
×
637

638
    protected override void TryExtractSupportingSQLTableImpl(SupportingSQLTable sqlTable, DirectoryInfo directory,
639
        IExtractionConfiguration configuration, IDataLoadEventListener listener, out int linesWritten,
640
        out string destinationDescription)
641
    {
642
        listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Information,
2✔
643
            $"About to download SQL for global SupportingSQL {sqlTable.SQL}"));
2✔
644
        using var con = sqlTable.GetServer().GetConnection();
2✔
645
        con.Open();
2✔
646

647
        listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Information,
2✔
648
            $"Connection opened successfully, about to send SQL command {sqlTable.SQL}"));
2✔
649

650
        using var dt = new DataTable();
2✔
651
        using (var cmd = DatabaseCommandHelper.GetCommand(sqlTable.SQL, con))
2✔
652
        using (var da = DatabaseCommandHelper.GetDataAdapter(cmd))
2✔
653
        {
654
            var sw = Stopwatch.StartNew();
2✔
655
            dt.BeginLoadData();
2✔
656
            da.Fill(dt);
2✔
657
            dt.EndLoadData();
2✔
658
        }
2✔
659

660
        dt.TableName = GetTableName(_destinationDatabase.Server.GetQuerySyntaxHelper()
2✔
661
            .GetSensibleEntityNameFromString(sqlTable.Name));
2✔
662
        linesWritten = dt.Rows.Count;
2✔
663

664
        var destinationDb = GetDestinationDatabase(listener);
2✔
665
        var tbl = destinationDb.ExpectTable(dt.TableName);
2✔
666

667
        if (tbl.Exists())
2!
668
            tbl.Drop();
×
669

670
        destinationDb.CreateTable(dt.TableName, dt);
2✔
671
        destinationDescription = $"{TargetDatabaseServer.ID}|{GetDatabaseName()}|{dt.TableName}";
2✔
672
    }
4✔
673

674

675
    protected override void TryExtractLookupTableImpl(BundledLookupTable lookup, DirectoryInfo lookupDir,
676
        IExtractionConfiguration requestConfiguration, IDataLoadEventListener listener, out int linesWritten,
677
        out string destinationDescription)
678
    {
679
        using var dt = lookup.GetDataTable();
1✔
680

681
        dt.TableName = GetTableName(_destinationDatabase.Server.GetQuerySyntaxHelper()
1✔
682
            .GetSensibleEntityNameFromString(lookup.TableInfo.Name));
1✔
683

684
        //describe the destination for the abstract base
685
        destinationDescription = $"{TargetDatabaseServer.ID}|{GetDatabaseName()}|{dt.TableName}";
1✔
686
        linesWritten = dt.Rows.Count;
1✔
687

688
        var destinationDb = GetDestinationDatabase(listener);
1✔
689
        var existing = destinationDb.ExpectTable(dt.TableName);
1✔
690

691
        if (existing.Exists())
1!
692
        {
693
            listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Warning,
×
694
                $"Dropping existing Lookup table '{existing.GetFullyQualifiedName()}'"));
×
695
            existing.Drop();
×
696
        }
697

698
        destinationDb.CreateTable(dt.TableName, dt);
1✔
699
    }
2✔
700

701
    private DiscoveredDatabase GetDestinationDatabase(IDataLoadEventListener listener)
702
    {
703
        //tell user we are about to inspect it
704
        listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Information,
73✔
705
            $"About to open connection to {TargetDatabaseServer}"));
73✔
706

707
        var databaseName = GetDatabaseName();
73✔
708

709
        var discoveredServer = DataAccessPortal.ExpectServer(TargetDatabaseServer, DataAccessContext.DataExport, false);
73✔
710

711
        var db = discoveredServer.ExpectDatabase(databaseName);
73✔
712
        if (!db.Exists())
73✔
713
            db.Create();
5✔
714

715
        return db;
73✔
716
    }
717

718
    private string GetDatabaseName()
719
    {
720
        var dbName = DatabaseNamingPattern;
148✔
721

722
        if (_project.ProjectNumber == null)
148!
723
            throw new ProjectNumberException($"Project '{_project}' must have a ProjectNumber");
×
724

725
        if (_request == null)
148!
726
            throw new Exception("No IExtractCommand Request was passed to this component");
×
727

728
        if (_request.Configuration == null)
148!
729
            throw new Exception($"Request did not specify any Configuration for Project '{_project}'");
×
730

731
        dbName = dbName.Replace("$p", _project.Name)
148✔
732
            .Replace("$n", _project.ProjectNumber.ToString())
148✔
733
            .Replace("$t", _project.MasterTicket)
148✔
734
            .Replace("$r", _request.Configuration.RequestTicket)
148✔
735
            .Replace("$l", _request.Configuration.ReleaseTicket)
148✔
736
            .Replace("$e", _request.Configuration.ID.ToString());
148✔
737
        return dbName;
148✔
738
    }
739

740
    public override void Check(ICheckNotifier notifier)
741
    {
742
        if (TargetDatabaseServer == null)
5✔
743
        {
744
            notifier.OnCheckPerformed(new CheckEventArgs(
1✔
745
                "Target database server property has not been set (This component does not know where to extract data to!), " +
1✔
746
                "to fix this you must edit the pipeline and choose an ExternalDatabaseServer to extract to)",
1✔
747
                CheckResult.Fail));
1✔
748
            return;
1✔
749
        }
750

751
        if (string.IsNullOrWhiteSpace(TargetDatabaseServer.Server))
4✔
752
        {
753
            notifier.OnCheckPerformed(new CheckEventArgs("TargetDatabaseServer does not have a .Server specified",
1✔
754
                CheckResult.Fail));
1✔
755
            return;
1✔
756
        }
757

758
        if (!string.IsNullOrWhiteSpace(TargetDatabaseServer.Database))
3!
759
            notifier.OnCheckPerformed(new CheckEventArgs(
×
760
                "TargetDatabaseServer has .Database specified but this will be ignored!", CheckResult.Warning));
×
761

762
        if (string.IsNullOrWhiteSpace(TableNamingPattern))
3!
763
        {
764
            notifier.OnCheckPerformed(new CheckEventArgs(
×
765
                "You must specify TableNamingPattern, this will tell the component how to name tables it generates in the remote destination",
×
766
                CheckResult.Fail));
×
767
            return;
×
768
        }
769

770
        if (string.IsNullOrWhiteSpace(DatabaseNamingPattern))
3!
771
        {
772
            notifier.OnCheckPerformed(new CheckEventArgs(
×
773
                "You must specify DatabaseNamingPattern, this will tell the component what database to create or use in the remote destination",
×
774
                CheckResult.Fail));
×
775
            return;
×
776
        }
777

778
        if (!DatabaseNamingPattern.Contains("$p") && !DatabaseNamingPattern.Contains("$n") &&
3✔
779
            !DatabaseNamingPattern.Contains("$t") && !DatabaseNamingPattern.Contains("$r") &&
3✔
780
            !DatabaseNamingPattern.Contains("$l"))
3✔
781
            notifier.OnCheckPerformed(new CheckEventArgs(
2✔
782
                "DatabaseNamingPattern does not contain any token. The tables may be created alongside existing tables and Release would be impossible.",
2✔
783
                CheckResult.Warning));
2✔
784

785
        if (!TableNamingPattern.Contains("$d") && !TableNamingPattern.Contains("$a"))
3!
786
            notifier.OnCheckPerformed(new CheckEventArgs(
×
787
                "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",
×
788
                CheckResult.Warning));
×
789

790
        if (_request == ExtractDatasetCommand.EmptyCommand)
3!
791
        {
792
            notifier.OnCheckPerformed(new CheckEventArgs(
×
793
                "Request is ExtractDatasetCommand.EmptyCommand, will not try to connect to Database",
×
794
                CheckResult.Warning));
×
795
            return;
×
796
        }
797

798
        if (TableNamingPattern != null && TableNamingPattern.Contains("$a"))
3!
799
            if (_request is ExtractDatasetCommand dsRequest && string.IsNullOrWhiteSpace(dsRequest.Catalogue.Acronym))
×
800
                notifier.OnCheckPerformed(new CheckEventArgs(
×
801
                    $"Catalogue '{dsRequest.Catalogue}' does not have an Acronym but TableNamingPattern contains $a",
×
802
                    CheckResult.Fail));
×
803

804

805

806
        base.Check(notifier);
3✔
807

808
        try
809
        {
810
            var server = DataAccessPortal.ExpectServer(TargetDatabaseServer, DataAccessContext.DataExport, false);
3✔
811
            var database = _destinationDatabase = server.ExpectDatabase(GetDatabaseName());
3✔
812

813
            if (UseArchiveTrigger)
3!
814
            {
815
                if (_request is ExtractDatasetCommand dsRequest)
×
816
                {
817
                    var existing = _destinationDatabase.ExpectTable(dsRequest.Catalogue.Name);
×
818
                    if (existing.Exists())
×
819
                    {
820
                        var hasPKs = existing.DiscoverColumns().Any(col => col.IsPrimaryKey);
×
821
                        if (!hasPKs)
×
822
                        {
823
                            notifier.OnCheckPerformed(new CheckEventArgs(
×
824
                               $"Catalogue does not have any PKS. Cannot apply the archive trigger",
×
825
                               CheckResult.Fail));
×
826
                        }
827
                    }
828
                }
829
            }
830

831
            if (database.Exists())
3✔
832
            {
833
                notifier.OnCheckPerformed(
1✔
834
                    new CheckEventArgs(
1✔
835
                        $"Database {database} already exists! if an extraction has already been run you may have errors if you are re-extracting the same tables",
1✔
836
                        CheckResult.Warning));
1✔
837
            }
838
            else
839
            {
840
                notifier.OnCheckPerformed(
2✔
841
                    new CheckEventArgs(
2✔
842
                        $"Database {database} does not exist on server... it will be created at runtime",
2✔
843
                        CheckResult.Success));
2✔
844
                return;
2✔
845
            }
846

847
            var tables = database.DiscoverTables(false);
1✔
848

849
            if (tables.Any())
1!
850
            {
851
                string tableName;
852

853
                try
854
                {
855
                    tableName = GetTableName();
×
856
                }
×
857
                catch (Exception ex)
×
858
                {
859
                    notifier.OnCheckPerformed(
×
860
                        new CheckEventArgs("Could not determine table name", CheckResult.Fail, ex));
×
861
                    return;
×
862
                }
863

864
                // if the expected table exists and we are not doing a batch resume or allowing data appending
865
                if (tables.Any(t => t.GetRuntimeName().Equals(tableName)) && !_request.IsBatchResume && !AppendDataIfTableExists)
×
866
                    notifier.OnCheckPerformed(new CheckEventArgs(ErrorCodes.ExistingExtractionTableInDatabase,
×
867
                        tableName, database));
×
868
            }
869
            else
870
            {
871
                notifier.OnCheckPerformed(new CheckEventArgs($"Confirmed that database {database} is empty of tables",
1✔
872
                    CheckResult.Success));
1✔
873
            }
874
        }
1✔
875
        catch (Exception e)
×
876
        {
877
            notifier.OnCheckPerformed(new CheckEventArgs(
×
878
                $"Could not connect to TargetDatabaseServer '{TargetDatabaseServer}'", CheckResult.Fail, e));
×
879
        }
×
880
    }
3✔
881
}
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