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

HicServices / RDMP / 9758065908

02 Jul 2024 09:03AM UTC coverage: 56.679% (-0.2%) from 56.914%
9758065908

push

github

web-flow
Release/8.2.0 (#1867)

* add extraction additions

* interim

* add test

* interim

* working dedupe

* improved checking

* add timestamp option

* fix extra looping

* add check

* start on tests

* tidy up code

* update link

* tidy up

* Rename executeFullExtractionToDatabaseMSSql.md to ExecuteFullExtractionToDatabaseMSSql.md

* fix typo

* add docs

* update

* update documentation

* attempt fix docs

* update docs

* tidy up code

* better tests

* add real test

* tidy up

* interim

* grab existiing entity

* no new data

* add basic tests

* attempt to fix test

* interim

* interim commit

* working clash

* add test

* fix test

* improved clash checker

* tidy up

* update test

* fix up test

* update from codeql

* tidy up code

* fix bad merge

* fix typo

* skip over for now

* revert change

* Task/RDMP-180 Add instance settings table (#1820)

* working settings interface

* add documentation

* add missing files

* update namespace

* add icon

* update from run

* make key unique

* add tests

* update tests

* update for tests

* fix unique name issue

* tidy up

* tidy up from review

* works

* nested deprications

* recursive deprication

* tidy up

* add newline

* Task/rdmp 174 dqe improvements (#1849)

* working scallable graph

* add changelog

* add axis override

* interim

* working increments

* working ui refresh

* update changelog

* tidy up code

* add missing file

* tidy up

* Task/rdmp 155 migrate catalogue tables (#1805)

* start of UI

* interim

* working switch

* improved ui

* fix build

* rename duped file

* imterim

* add checks

* start of tests

* local tests  working

* add tests

* improved ui

* tidy up

* add single item use

* broken test

* updated tests

* tidy up imports

* add some documentation

* fix docume... (continued)

10912 of 20750 branches covered (52.59%)

Branch coverage included in aggregate %.

369 of 831 new or added lines in 38 files covered. (44.4%)

375 existing lines in 25 files now uncovered.

30965 of 53135 relevant lines covered (58.28%)

7845.71 hits per line

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

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

31
namespace Rdmp.Core.DataExport.DataExtraction.Pipeline.Destinations;
32

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

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

52
    [DemandsInitialization(@"How do you want to name datasets, use the following tokens if you need them:   
53
         $p - Project Name ('e.g. My Project')
54
         $n - Project Number (e.g. 234)
55
         $c - Configuration Name (e.g. 'Cases')
56
         $d - Dataset name (e.g. 'Prescribing')
57
         $a - Dataset acronym (e.g. 'Presc') 
58

59
         You must have either $a or $d
60
         ", Mandatory = true, DefaultValue = "$c_$d")]
61
    public string TableNamingPattern { get; set; }
102✔
62

63
    [DemandsInitialization(
64
        @"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 ",
65
        defaultValue: true)]
66
    public bool DropTableIfLoadFails { get; set; }
44✔
67

68
    [DemandsInitialization(DataTableUploadDestination.AlterTimeout_Description, DefaultValue = 300)]
69
    public int AlterTimeout { get; set; }
66✔
70

71
    [DemandsInitialization(
72
        "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.",
73
        DefaultValue = false)]
74
    public bool CopyCollations { get; set; }
808✔
75

76
    [DemandsInitialization(
77
        "True to always drop the destination database table(s) from the destination if they already existed",
78
        DefaultValue = false)]
79
    public bool AlwaysDropExtractionTables { get; set; }
54✔
80

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

86

87
    [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.")]
88
    public bool AppendDataIfTableExists { get; set; } = false;
88✔
89

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

93

94
    [DemandsInitialization("If chekced, indexed will be created using the primary keys specified")]
95
    public bool IndexTables { get; set; } = true;
120✔
96

97
    [DemandsInitialization(@"How do you want to name the created index, use the following tokens if you need them:   
98
         $p - Project Name ('e.g. My Project')
99
         $n - Project Number (e.g. 234)
100
         $c - Configuration Name (e.g. 'Cases')
101
         $d - Dataset name (e.g. 'Prescribing')
102
         $a - Dataset acronym (e.g. 'Presc') 
103

104
         You must have either $a or $d
105
         ",DefaultValue = "Index_$c_$d")]
106
    public string IndexNamingPattern { get; set; }
66✔
107

108
    [DemandsInitialization("An optional list of columns to index on e.g \"Column1, Column2\"")]
109
    public string UserDefinedIndex { get; set; }
66✔
110

111
    private DiscoveredDatabase _destinationDatabase;
112
    private DataTableUploadDestination _destination;
113

114
    private bool _tableDidNotExistAtStartOfLoad;
115
    private bool _isTableAlreadyNamed;
116
    private DataTable _toProcess;
117

118
    public ExecuteFullExtractionToDatabaseMSSql() : base(false)
54✔
119
    {
120
    }
54✔
121

122
    public override DataTable ProcessPipelineData(DataTable toProcess, IDataLoadEventListener job,
123
        GracefulCancellationToken cancellationToken)
124
    {
125
        _destinationDatabase = GetDestinationDatabase(job);
44✔
126
        return base.ProcessPipelineData(toProcess, job, cancellationToken);
44✔
127
    }
128

129
    protected override void Open(DataTable toProcess, IDataLoadEventListener job,
130
        GracefulCancellationToken cancellationToken)
131
    {
132
        _toProcess = toProcess;
22✔
133

134
        //give the data table the correct name
135
        if (_toProcess.ExtendedProperties.ContainsKey("ProperlyNamed") &&
22!
136
            _toProcess.ExtendedProperties["ProperlyNamed"].Equals(true))
22✔
137
            _isTableAlreadyNamed = true;
×
138

139
        _toProcess.TableName = GetTableName();
22✔
140

141
        _destination = PrepareDestination(job, _toProcess);
22✔
142
        OutputFile = _toProcess.TableName;
22✔
143
    }
22✔
144

145
    protected override void WriteRows(DataTable toProcess, IDataLoadEventListener job,
146
        GracefulCancellationToken cancellationToken, Stopwatch stopwatch)
147
    {
148
        // empty batches are allowed when using batch/resume
149
        if (toProcess.Rows.Count == 0 && _request.IsBatchResume) return;
22!
150

151
        if (_request.IsBatchResume) _destination.AllowLoadingPopulatedTables = true;
22!
152

153
        _destination.ProcessPipelineData(toProcess, job, cancellationToken);
22✔
154

155
        LinesWritten += toProcess.Rows.Count;
22✔
156
    }
22✔
157

158
    private DataTableUploadDestination PrepareDestination(IDataLoadEventListener listener, DataTable toProcess)
159
    {
160
        //see if the user has entered an extraction server/database
161
        if (TargetDatabaseServer == null)
22!
162
            throw new Exception(
×
163
                "TargetDatabaseServer (the place you want to extract the project data to) property has not been set!");
×
164

165
        try
166
        {
167
            if (!_destinationDatabase.Exists())
22!
168
                _destinationDatabase.Create();
×
169

170
            if (_request is ExtractGlobalsCommand)
22!
171
                return null;
×
172

173
            var tblName = _toProcess.TableName;
22✔
174

175
            //See if table already exists on the server (likely to cause problems including duplication, schema changes in configuration etc)
176
            var existing = _destinationDatabase.ExpectTable(tblName);
22✔
177
            if (existing.Exists())
22✔
178
            {
179
                if (_request.IsBatchResume)
10!
180
                {
181
                    listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Information,
×
182
                        $"Table {existing.GetFullyQualifiedName()} already exists but it IsBatchResume so no problem."));
×
183
                }
184
                else if (AlwaysDropExtractionTables)
10!
185
                {
186
                    listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Warning,
×
187
                        $"Table {existing.GetFullyQualifiedName()} already exists, dropping because setting {nameof(AlwaysDropExtractionTables)} is on"));
×
188
                    existing.Drop();
×
189

190
                    listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Warning,
×
191
                        $"Table {existing.GetFullyQualifiedName()} was dropped"));
×
192

193
                    // since we dropped it we should treat it as if it was never there to begin with
194
                    _tableDidNotExistAtStartOfLoad = true;
×
195
                }
196
                else
197
                {
198
                    listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Warning,
10✔
199
                        $"A table called {tblName} already exists on server {TargetDatabaseServer}, data load might crash if it is populated and/or has an incompatible schema"));
10✔
200
                }
201
            }
202
            else
203
            {
204
                _tableDidNotExistAtStartOfLoad = true;
12✔
205
            }
206
        }
22✔
207
        catch (Exception e)
×
208
        {
209
            //Probably the database didn't exist or the credentials were wrong or something
210
            listener.OnNotify(this,
×
211
                new NotifyEventArgs(ProgressEventType.Error,
×
212
                    "Failed to inspect destination for already existing datatables", e));
×
213
        }
×
214

215
        _destination = new DataTableUploadDestination(((IExtractDatasetCommand)_request).ExtractableCohort.ExternalCohortTable);
22✔
216

217
        PrimeDestinationTypesBasedOnCatalogueTypes(listener, toProcess);
22✔
218

219
        _destination.AllowResizingColumnsAtUploadTime = true;
22✔
220
        _destination.AlterTimeout = AlterTimeout;
22✔
221
        _destination.AppendDataIfTableExists = AppendDataIfTableExists;
22✔
222
        _destination.IncludeTimeStamp = IncludeTimeStamp;
22✔
223
        _destination.UseTrigger = AppendDataIfTableExists;
22✔
224
        _destination.IndexTables = IndexTables;
22✔
225
        _destination.IndexTableName = GetIndexName();
22✔
226
        if (UserDefinedIndex is not null)
22!
NEW
227
            _destination.UserDefinedIndexes = UserDefinedIndex.Split(',').Select(i => i.Trim()).ToList();
×
228
        _destination.PreInitialize(_destinationDatabase, listener);
22✔
229

230
        return _destination;
22✔
231
    }
×
232

233
    private void PrimeDestinationTypesBasedOnCatalogueTypes(IDataLoadEventListener listener, DataTable toProcess)
234
    {
235
        //if the extraction is of a Catalogue
236

237
        if (_request is not IExtractDatasetCommand datasetCommand)
22!
238
            return;
×
239

240
        //for every extractable column in the Catalogue
241
        foreach (var extractionInformation in datasetCommand.ColumnsToExtract.OfType<ExtractableColumn>()
1,576✔
242
                     .Select(ec =>
22✔
243
                         ec.CatalogueExtractionInformation))
788✔
244
        {
245
            if (extractionInformation == null)
766✔
246
                continue;
247

248
            var catItem = extractionInformation.CatalogueItem;
766✔
249

250
            //if we do not know the data type or the ei is a transform
251
            if (catItem == null)
766!
252
            {
253
                listener.OnNotify(this,
×
254
                    new NotifyEventArgs(ProgressEventType.Warning,
×
255
                        $"Did not copy Types for ExtractionInformation {extractionInformation} (ID={extractionInformation.ID}) because it had no associated CatalogueItem"));
×
256
                continue;
×
257
            }
258

259
            if (catItem.ColumnInfo == null)
766!
260
            {
261
                listener.OnNotify(this,
×
262
                    new NotifyEventArgs(ProgressEventType.Warning,
×
263
                        $"Did not copy Types for ExtractionInformation {extractionInformation} (ID={extractionInformation.ID}) because it had no associated ColumnInfo"));
×
264
                continue;
×
265
            }
266

267
            if (extractionInformation.IsProperTransform())
766✔
268
            {
269
                listener.OnNotify(this,
2✔
270
                    new NotifyEventArgs(ProgressEventType.Warning,
2✔
271
                        $"Did not copy Types for ExtractionInformation {extractionInformation} (ID={extractionInformation.ID}) because it is a Transform"));
2✔
272
                continue;
2✔
273
            }
274

275
            var destinationType = GetDestinationDatabaseType(extractionInformation);
764✔
276

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

284
            //if user wants to copy collation types and the destination server is the same type as the origin server
285
            if (CopyCollations && _destinationDatabase.Server.DatabaseType == catItem.ColumnInfo.TableInfo.DatabaseType)
764!
286
                addedType.Collation = catItem.ColumnInfo.Collation;
×
287

288
            listener.OnNotify(this,
764✔
289
                new NotifyEventArgs(ProgressEventType.Information,
764✔
290
                    $"Set Type for {columnName} to {destinationType} (IsPrimaryKey={(addedType.IsPrimaryKey ? "true" : "false")}) to match the source table"));
764✔
291
        }
292

293

294
        foreach (var sub in datasetCommand.QueryBuilder.SelectColumns.Select(static sc => sc.IColumn)
876✔
295
                     .OfType<ReleaseIdentifierSubstitution>())
22✔
296
        {
297
            var columnName = sub.GetRuntimeName();
22✔
298
            var isPk = toProcess.PrimaryKey.Any(dc => dc.ColumnName == columnName);
44✔
299

300
            var addedType = _destination.AddExplicitWriteType(columnName,
22✔
301
                datasetCommand.ExtractableCohort.GetReleaseIdentifierDataType());
22✔
302
            addedType.IsPrimaryKey = isPk;
22✔
303
            addedType.AllowNulls = !isPk;
22✔
304
        }
305
    }
22✔
306

307
    private string GetDestinationDatabaseType(ConcreteColumn col)
308
    {
309
        //Make sure we know if we are going between database types
310
        var fromDbType = _destinationDatabase.Server.DatabaseType;
764✔
311
        var toDbType = col.ColumnInfo.TableInfo.DatabaseType;
764✔
312
        if (fromDbType != toDbType)
764!
313
        {
314
            var fromSyntax = col.ColumnInfo.GetQuerySyntaxHelper();
×
315
            var toSyntax = _destinationDatabase.Server.GetQuerySyntaxHelper();
×
316

317
            var intermediate = fromSyntax.TypeTranslater.GetDataTypeRequestForSQLDBType(col.ColumnInfo.Data_type);
×
318
            return toSyntax.TypeTranslater.GetSQLDBTypeForCSharpType(intermediate);
×
319
        }
320

321
        return col.ColumnInfo.Data_type;
764✔
322
    }
323

324
    private string GetIndexName()
325
    {
326
        string indexName = IndexNamingPattern;
22✔
327
        var project = _request.Configuration.Project;
22✔
328
        indexName = indexName.Replace("$p", project.Name);
22✔
329
        indexName = indexName.Replace("$n", project.ProjectNumber.ToString());
22✔
330
        indexName = indexName.Replace("$c", _request.Configuration.Name);
22✔
331
        if (_request is ExtractDatasetCommand extractDatasetCommand)
22✔
332
        {
333
            indexName = indexName.Replace("$d", extractDatasetCommand.DatasetBundle.DataSet.Catalogue.Name);
22✔
334
            indexName = indexName.Replace("$a", extractDatasetCommand.DatasetBundle.DataSet.Catalogue.Acronym);
22✔
335
        }
336

337
        if (_request is ExtractGlobalsCommand)
22!
338
        {
NEW
339
            indexName = indexName.Replace("$d", ExtractionDirectory.GLOBALS_DATA_NAME);
×
NEW
340
            indexName = indexName.Replace("$a", "G");
×
341
        }
342

343

344
        return indexName.Replace(" ","");
22✔
345
    }
346

347
    private string GetTableName(string suffix = null)
348
    {
349
        string tblName;
350
        if (_isTableAlreadyNamed)
28!
351
        {
352
            tblName = SanitizeNameForDatabase(_toProcess.TableName);
×
353

354
            if (!string.IsNullOrWhiteSpace(suffix))
×
355
                tblName += $"_{suffix}";
×
356

357
            return tblName;
×
358
        }
359

360
        tblName = TableNamingPattern;
28✔
361
        var project = _request.Configuration.Project;
28✔
362

363
        tblName = tblName.Replace("$p", project.Name);
28✔
364
        tblName = tblName.Replace("$n", project.ProjectNumber.ToString());
28✔
365
        tblName = tblName.Replace("$c", _request.Configuration.Name);
28✔
366

367
        if (_request is ExtractDatasetCommand extractDatasetCommand)
28✔
368
        {
369
            tblName = tblName.Replace("$d", extractDatasetCommand.DatasetBundle.DataSet.Catalogue.Name);
26✔
370
            tblName = tblName.Replace("$a", extractDatasetCommand.DatasetBundle.DataSet.Catalogue.Acronym);
26✔
371
        }
372

373
        if (_request is ExtractGlobalsCommand)
28✔
374
        {
375
            tblName = tblName.Replace("$d", ExtractionDirectory.GLOBALS_DATA_NAME);
2✔
376
            tblName = tblName.Replace("$a", "G");
2✔
377
        }
378

379
        var cachedGetTableNameAnswer = SanitizeNameForDatabase(tblName);
28✔
380
        if (!string.IsNullOrWhiteSpace(suffix))
28✔
381
            cachedGetTableNameAnswer += $"_{suffix}";
6✔
382

383
        return cachedGetTableNameAnswer;
28✔
384
    }
385

386
    private string SanitizeNameForDatabase(string tblName)
387
    {
388
        if (_destinationDatabase == null)
28!
389
            throw new Exception(
×
390
                "Cannot pick a TableName until we know what type of server it is going to, _server is null");
×
391

392
        //get rid of brackets and dots
393
        tblName = Regex.Replace(tblName, "[.()]", "_");
28✔
394

395
        var syntax = _destinationDatabase.Server.GetQuerySyntaxHelper();
28✔
396
        syntax.ValidateTableName(tblName);
28✔
397

398
        //otherwise, fetch and cache answer
399
        var cachedGetTableNameAnswer = syntax.GetSensibleEntityNameFromString(tblName);
28✔
400

401
        return string.IsNullOrWhiteSpace(cachedGetTableNameAnswer)
28!
402
            ? throw new Exception(
28✔
403
                $"TableNamingPattern '{TableNamingPattern}' resulted in an empty string for request '{_request}'")
28✔
404
            : cachedGetTableNameAnswer;
28✔
405
    }
406

407
    public override void Dispose(IDataLoadEventListener listener, Exception pipelineFailureExceptionIfAny)
408
    {
409
        if (_destination != null)
44✔
410
        {
411
            _destination.Dispose(listener, pipelineFailureExceptionIfAny);
22✔
412

413
            //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
414
            if (pipelineFailureExceptionIfAny != null && _tableDidNotExistAtStartOfLoad && DropTableIfLoadFails)
22!
415
                if (_destinationDatabase != null)
×
416
                {
417
                    var tbl = _destinationDatabase.ExpectTable(_toProcess.TableName);
×
418

419
                    if (tbl.Exists())
×
420
                    {
421
                        listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Warning,
×
422
                            $"DropTableIfLoadFails is true so about to drop table {tbl}"));
×
423
                        tbl.Drop();
×
424
                        listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Warning, $"Dropped table {tbl}"));
×
425
                    }
426
                }
427

428
            if (pipelineFailureExceptionIfAny == null
22!
429
                && _request.IsBatchResume
22✔
430
                && MakeFinalTableDistinctWhenBatchResuming
22✔
431
                && _destinationDatabase != null
22✔
432
                && _toProcess != null)
22✔
433
            {
434
                var tbl = _destinationDatabase.ExpectTable(_toProcess.TableName);
×
435
                if (tbl.Exists())
×
436
                    // if there is no primary key then failed batches may have introduced duplication
437
                    if (!tbl.DiscoverColumns().Any(p => p.IsPrimaryKey))
×
438
                    {
439
                        listener.OnNotify(this,
×
440
                            new NotifyEventArgs(ProgressEventType.Information,
×
441
                                $"Making {tbl} distinct in case there are duplicate rows from bad batch resumes"));
×
442
                        tbl.MakeDistinct(50000000);
×
443
                        listener.OnNotify(this,
×
444
                            new NotifyEventArgs(ProgressEventType.Information, $"Finished distincting {tbl}"));
×
445
                    }
446
            }
447
        }
448

449
        TableLoadInfo?.CloseAndArchive();
44✔
450

451
        // also close off the cumulative extraction result
452
        if (_request is ExtractDatasetCommand)
44✔
453
        {
454
            var result = ((IExtractDatasetCommand)_request).CumulativeExtractionResults;
22✔
455
            if (result != null && _toProcess != null)
22✔
456
                result.CompleteAudit(GetType(), GetDestinationDescription(), TableLoadInfo.Inserts,
22✔
457
                    _request.IsBatchResume, pipelineFailureExceptionIfAny != null);
22✔
458
        }
459
    }
44✔
460

461
    public override void Abort(IDataLoadEventListener listener)
462
    {
463
        _destination?.Abort(listener);
×
464
    }
×
465

466
    protected override void PreInitializeImpl(IExtractCommand value, IDataLoadEventListener listener)
467
    {
468
    }
94✔
469

470

471
    public override string GetDestinationDescription() => GetDestinationDescription("");
44✔
472

473
    private string GetDestinationDescription(string suffix = "")
474
    {
475
        if (_toProcess == null)
66✔
476
            return _request is ExtractGlobalsCommand
22!
477
                ? "Globals"
22✔
478
                : throw new Exception("Could not describe destination because _toProcess was null");
22✔
479

480
        var tblName = _toProcess.TableName;
44✔
481
        var dbName = GetDatabaseName();
44✔
482
        return $"{TargetDatabaseServer.ID}|{dbName}|{tblName}";
44✔
483
    }
484

485
    public static DestinationType GetDestinationType() => DestinationType.Database;
×
486

487
    public override ReleasePotential GetReleasePotential(IRDMPPlatformRepositoryServiceLocator repositoryLocator,
488
        ISelectedDataSets selectedDataSet) => new MsSqlExtractionReleasePotential(repositoryLocator, selectedDataSet);
×
489

490
    public override FixedReleaseSource<ReleaseAudit> GetReleaseSource(ICatalogueRepository catalogueRepository) =>
491
        new MsSqlReleaseSource(catalogueRepository);
×
492

493
    public override GlobalReleasePotential GetGlobalReleasabilityEvaluator(
494
        IRDMPPlatformRepositoryServiceLocator repositoryLocator, ISupplementalExtractionResults globalResult,
495
        IMapsDirectlyToDatabaseTable globalToCheck) =>
496
        new MsSqlGlobalsReleasePotential(repositoryLocator, globalResult, globalToCheck);
×
497

498
    protected override void TryExtractSupportingSQLTableImpl(SupportingSQLTable sqlTable, DirectoryInfo directory,
499
        IExtractionConfiguration configuration, IDataLoadEventListener listener, out int linesWritten,
500
        out string destinationDescription)
501
    {
502
        listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Information,
4✔
503
            $"About to download SQL for global SupportingSQL {sqlTable.SQL}"));
4✔
504
        using var con = sqlTable.GetServer().GetConnection();
4✔
505
        con.Open();
4✔
506

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

510
        using var dt = new DataTable();
4✔
511
        using (var cmd = DatabaseCommandHelper.GetCommand(sqlTable.SQL, con))
4✔
512
        using (var da = DatabaseCommandHelper.GetDataAdapter(cmd))
4✔
513
        {
514
            var sw = Stopwatch.StartNew();
4✔
515
            dt.BeginLoadData();
4✔
516
            da.Fill(dt);
4✔
517
            dt.EndLoadData();
4✔
518
        }
4✔
519

520
        dt.TableName = GetTableName(_destinationDatabase.Server.GetQuerySyntaxHelper()
4✔
521
            .GetSensibleEntityNameFromString(sqlTable.Name));
4✔
522
        linesWritten = dt.Rows.Count;
4✔
523

524
        var destinationDb = GetDestinationDatabase(listener);
4✔
525
        var tbl = destinationDb.ExpectTable(dt.TableName);
4✔
526

527
        if (tbl.Exists())
4!
528
            tbl.Drop();
×
529

530
        destinationDb.CreateTable(dt.TableName, dt);
4✔
531
        destinationDescription = $"{TargetDatabaseServer.ID}|{GetDatabaseName()}|{dt.TableName}";
4✔
532
    }
8✔
533

534

535
    protected override void TryExtractLookupTableImpl(BundledLookupTable lookup, DirectoryInfo lookupDir,
536
        IExtractionConfiguration requestConfiguration, IDataLoadEventListener listener, out int linesWritten,
537
        out string destinationDescription)
538
    {
539
        using var dt = lookup.GetDataTable();
2✔
540

541
        dt.TableName = GetTableName(_destinationDatabase.Server.GetQuerySyntaxHelper()
2✔
542
            .GetSensibleEntityNameFromString(lookup.TableInfo.Name));
2✔
543

544
        //describe the destination for the abstract base
545
        destinationDescription = $"{TargetDatabaseServer.ID}|{GetDatabaseName()}|{dt.TableName}";
2✔
546
        linesWritten = dt.Rows.Count;
2✔
547

548
        var destinationDb = GetDestinationDatabase(listener);
2✔
549
        var existing = destinationDb.ExpectTable(dt.TableName);
2✔
550

551
        if (existing.Exists())
2!
552
        {
553
            listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Warning,
×
554
                $"Dropping existing Lookup table '{existing.GetFullyQualifiedName()}'"));
×
555
            existing.Drop();
×
556
        }
557

558
        destinationDb.CreateTable(dt.TableName, dt);
2✔
559
    }
4✔
560

561
    private DiscoveredDatabase GetDestinationDatabase(IDataLoadEventListener listener)
562
    {
563
        //tell user we are about to inspect it
564
        listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Information,
50✔
565
            $"About to open connection to {TargetDatabaseServer}"));
50✔
566

567
        var databaseName = GetDatabaseName();
50✔
568

569
        var discoveredServer = DataAccessPortal.ExpectServer(TargetDatabaseServer, DataAccessContext.DataExport, false);
50✔
570

571
        var db = discoveredServer.ExpectDatabase(databaseName);
50✔
572
        if (!db.Exists())
50✔
573
            db.Create();
10✔
574

575
        return db;
50✔
576
    }
577

578
    private string GetDatabaseName()
579
    {
580
        var dbName = DatabaseNamingPattern;
106✔
581

582
        if (_project.ProjectNumber == null)
106!
583
            throw new ProjectNumberException($"Project '{_project}' must have a ProjectNumber");
×
584

585
        if (_request == null)
106!
586
            throw new Exception("No IExtractCommand Request was passed to this component");
×
587

588
        if (_request.Configuration == null)
106!
589
            throw new Exception($"Request did not specify any Configuration for Project '{_project}'");
×
590

591
        dbName = dbName.Replace("$p", _project.Name)
106✔
592
            .Replace("$n", _project.ProjectNumber.ToString())
106✔
593
            .Replace("$t", _project.MasterTicket)
106✔
594
            .Replace("$r", _request.Configuration.RequestTicket)
106✔
595
            .Replace("$l", _request.Configuration.ReleaseTicket);
106✔
596

597
        return dbName;
106✔
598
    }
599

600
    public override void Check(ICheckNotifier notifier)
601
    {
602
        if (TargetDatabaseServer == null)
10✔
603
        {
604
            notifier.OnCheckPerformed(new CheckEventArgs(
2✔
605
                "Target database server property has not been set (This component does not know where to extract data to!), " +
2✔
606
                "to fix this you must edit the pipeline and choose an ExternalDatabaseServer to extract to)",
2✔
607
                CheckResult.Fail));
2✔
608
            return;
2✔
609
        }
610

611
        if (string.IsNullOrWhiteSpace(TargetDatabaseServer.Server))
8✔
612
        {
613
            notifier.OnCheckPerformed(new CheckEventArgs("TargetDatabaseServer does not have a .Server specified",
2✔
614
                CheckResult.Fail));
2✔
615
            return;
2✔
616
        }
617

618
        if (!string.IsNullOrWhiteSpace(TargetDatabaseServer.Database))
6!
619
            notifier.OnCheckPerformed(new CheckEventArgs(
×
620
                "TargetDatabaseServer has .Database specified but this will be ignored!", CheckResult.Warning));
×
621

622
        if (string.IsNullOrWhiteSpace(TableNamingPattern))
6!
623
        {
624
            notifier.OnCheckPerformed(new CheckEventArgs(
×
625
                "You must specify TableNamingPattern, this will tell the component how to name tables it generates in the remote destination",
×
626
                CheckResult.Fail));
×
627
            return;
×
628
        }
629

630
        if (string.IsNullOrWhiteSpace(DatabaseNamingPattern))
6!
631
        {
632
            notifier.OnCheckPerformed(new CheckEventArgs(
×
633
                "You must specify DatabaseNamingPattern, this will tell the component what database to create or use in the remote destination",
×
634
                CheckResult.Fail));
×
635
            return;
×
636
        }
637

638
        if (!DatabaseNamingPattern.Contains("$p") && !DatabaseNamingPattern.Contains("$n") &&
6✔
639
            !DatabaseNamingPattern.Contains("$t") && !DatabaseNamingPattern.Contains("$r") &&
6✔
640
            !DatabaseNamingPattern.Contains("$l"))
6✔
641
            notifier.OnCheckPerformed(new CheckEventArgs(
4✔
642
                "DatabaseNamingPattern does not contain any token. The tables may be created alongside existing tables and Release would be impossible.",
4✔
643
                CheckResult.Warning));
4✔
644

645
        if (!TableNamingPattern.Contains("$d") && !TableNamingPattern.Contains("$a"))
6!
646
            notifier.OnCheckPerformed(new CheckEventArgs(
×
647
                "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",
×
648
                CheckResult.Warning));
×
649

650
        if (_request == ExtractDatasetCommand.EmptyCommand)
6!
651
        {
652
            notifier.OnCheckPerformed(new CheckEventArgs(
×
653
                "Request is ExtractDatasetCommand.EmptyCommand, will not try to connect to Database",
×
654
                CheckResult.Warning));
×
655
            return;
×
656
        }
657

658
        if (TableNamingPattern != null && TableNamingPattern.Contains("$a"))
6!
659
            if (_request is ExtractDatasetCommand dsRequest && string.IsNullOrWhiteSpace(dsRequest.Catalogue.Acronym))
×
660
                notifier.OnCheckPerformed(new CheckEventArgs(
×
661
                    $"Catalogue '{dsRequest.Catalogue}' does not have an Acronym but TableNamingPattern contains $a",
×
662
                    CheckResult.Fail));
×
663

664
        base.Check(notifier);
6✔
665

666
        try
667
        {
668
            var server = DataAccessPortal.ExpectServer(TargetDatabaseServer, DataAccessContext.DataExport, false);
6✔
669
            var database = _destinationDatabase = server.ExpectDatabase(GetDatabaseName());
6✔
670

671
            if (database.Exists())
6✔
672
            {
673
                notifier.OnCheckPerformed(
2✔
674
                    new CheckEventArgs(
2✔
675
                        $"Database {database} already exists! if an extraction has already been run you may have errors if you are re-extracting the same tables",
2✔
676
                        CheckResult.Warning));
2✔
677
            }
678
            else
679
            {
680
                notifier.OnCheckPerformed(
4✔
681
                    new CheckEventArgs(
4✔
682
                        $"Database {database} does not exist on server... it will be created at runtime",
4✔
683
                        CheckResult.Success));
4✔
684
                return;
4✔
685
            }
686

687
            var tables = database.DiscoverTables(false);
2✔
688

689
            if (tables.Any())
2!
690
            {
691
                string tableName;
692

693
                try
694
                {
695
                    tableName = GetTableName();
×
696
                }
×
697
                catch (Exception ex)
×
698
                {
699
                    notifier.OnCheckPerformed(
×
700
                        new CheckEventArgs("Could not determine table name", CheckResult.Fail, ex));
×
701
                    return;
×
702
                }
703

704
                // if the expected table exists and we are not doing a batch resume or allowing data appending
NEW
705
                if (tables.Any(t => t.GetRuntimeName().Equals(tableName)) && !_request.IsBatchResume && !AppendDataIfTableExists)
×
706
                    notifier.OnCheckPerformed(new CheckEventArgs(ErrorCodes.ExistingExtractionTableInDatabase,
×
707
                        tableName, database));
×
708
            }
709
            else
710
            {
711
                notifier.OnCheckPerformed(new CheckEventArgs($"Confirmed that database {database} is empty of tables",
2✔
712
                    CheckResult.Success));
2✔
713
            }
714
        }
2✔
715
        catch (Exception e)
×
716
        {
717
            notifier.OnCheckPerformed(new CheckEventArgs(
×
718
                $"Could not connect to TargetDatabaseServer '{TargetDatabaseServer}'", CheckResult.Fail, e));
×
719
        }
×
720
    }
6✔
721
}
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