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

HicServices / RDMP / 9988359965

18 Jul 2024 08:42AM UTC coverage: 57.299% (+0.6%) from 56.679%
9988359965

push

github

JFriel
Merge branch 'develop' of https://github.com/HicServices/RDMP

11072 of 20790 branches covered (53.26%)

Branch coverage included in aggregate %.

31313 of 53181 relevant lines covered (58.88%)

7885.96 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.MapsDirectlyToDatabaseTable;
23
using Rdmp.Core.QueryBuilding;
24
using Rdmp.Core.Repositories;
25
using Rdmp.Core.ReusableLibraryCode;
26
using Rdmp.Core.ReusableLibraryCode.Checks;
27
using Rdmp.Core.ReusableLibraryCode.DataAccess;
28
using Rdmp.Core.ReusableLibraryCode.Progress;
29

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

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

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

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

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

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

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

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

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

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

85

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

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

92

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

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

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

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

110
    private DiscoveredDatabase _destinationDatabase;
111
    private DataTableUploadDestination _destination;
112

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

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

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

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

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

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

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

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

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

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

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

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

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

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

172
            var tblName = _toProcess.TableName;
22✔
173

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

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

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

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

216
        PrimeDestinationTypesBasedOnCatalogueTypes(listener, toProcess);
22✔
217

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

229
        return _destination;
22✔
230
    }
×
231

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

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

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

247
            var catItem = extractionInformation.CatalogueItem;
766✔
248

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

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

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

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

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

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

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

292

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

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

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

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

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

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

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

342

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

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

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

356
            return tblName;
×
357
        }
358

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

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

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

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

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

382
        return cachedGetTableNameAnswer;
28✔
383
    }
384

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

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

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

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

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

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

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

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

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

448
        TableLoadInfo?.CloseAndArchive();
44✔
449

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

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

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

469

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

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

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

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

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

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

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

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

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

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

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

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

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

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

533

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

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

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

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

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

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

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

566
        var databaseName = GetDatabaseName();
50✔
567

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

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

574
        return db;
50✔
575
    }
576

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

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

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

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

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

596
        return dbName;
106✔
597
    }
598

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

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

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

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

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

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

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

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

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

663
        base.Check(notifier);
6✔
664

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

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

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

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

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

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