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

HicServices / RDMP / 15133347244

20 May 2025 08:52AM UTC coverage: 57.504% (-0.001%) from 57.505%
15133347244

push

github

JFriel
fix set

11400 of 21380 branches covered (53.32%)

Branch coverage included in aggregate %.

32348 of 54698 relevant lines covered (59.14%)

17527.09 hits per line

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

68.5
/Rdmp.Core/DataExport/DataExtraction/Pipeline/Destinations/ExecuteFullExtractionToDatabaseMSSql.cs
1
// Copyright (c) The University of Dundee 2018-2024
2
// This file is part of the Research Data Management Platform (RDMP).
3
// RDMP is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
4
// RDMP is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
5
// You should have received a copy of the GNU General Public License along with RDMP. If not, see <https://www.gnu.org/licenses/>.
6

7
using 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
using YamlDotNet.Core;
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
         $e - Extraction Configuration Id (e.g. 14)
50
         ", Mandatory = true, DefaultValue = "Proj_$n_$l")]
51
    public string DatabaseNamingPattern { get; set; }
186✔
52

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

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

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

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

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

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

87

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

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

94

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

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

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

112
    private DiscoveredDatabase _destinationDatabase;
113
    private DataTableUploadDestination _destination;
114

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

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

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

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

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

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

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

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

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

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

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

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

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

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

174
            var tblName = _toProcess.TableName;
22✔
175

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

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

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

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

218
        PrimeDestinationTypesBasedOnCatalogueTypes(listener, toProcess);
22✔
219

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

231
        return _destination;
22✔
232
    }
×
233

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

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

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

249
            var catItem = extractionInformation.CatalogueItem;
766✔
250

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

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

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

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

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

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

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

294

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

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

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

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

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

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

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

345

346
        return indexName.Replace(" ","");
22✔
347
    }
348

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

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

359
            return tblName;
×
360
        }
361

362
        tblName = TableNamingPattern;
28✔
363
        var project = _request.Configuration.Project;
28✔
364

365
        tblName = tblName.Replace("$p", project.Name);
28✔
366
        tblName = tblName.Replace("$n", project.ProjectNumber.ToString());
28✔
367
        tblName = tblName.Replace("$c", _request.Configuration.Name);
28✔
368
        tblName = tblName.Replace("$e", _request.Configuration.ID.ToString());
28✔
369

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

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

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

386
        return cachedGetTableNameAnswer;
28✔
387
    }
388

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

395
        //get rid of brackets and dots
396
        tblName = Regex.Replace(tblName, "[.()]", "_");
28✔
397

398
        var syntax = _destinationDatabase.Server.GetQuerySyntaxHelper();
28✔
399
        syntax.ValidateTableName(tblName);
28✔
400

401
        //otherwise, fetch and cache answer
402
        var cachedGetTableNameAnswer = syntax.GetSensibleEntityNameFromString(tblName);
28✔
403

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

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

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

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

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

452
        TableLoadInfo?.CloseAndArchive();
44✔
453

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

464
    public override void Abort(IDataLoadEventListener listener)
465
    {
466
        _destination?.Abort(listener);
×
467
    }
×
468

469
    protected override void PreInitializeImpl(IExtractCommand value, IDataLoadEventListener listener)
470
    {
471
    }
94✔
472

473

474
    public override string GetDestinationDescription() => GetDestinationDescription("");
44✔
475

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

483
        var tblName = _toProcess.TableName;
44✔
484
        var dbName = GetDatabaseName();
44✔
485
        return $"{TargetDatabaseServer.ID}|{dbName}|{tblName}";
44✔
486
    }
487

488
    public static DestinationType GetDestinationType() => DestinationType.Database;
×
489

490
    public override ReleasePotential GetReleasePotential(IRDMPPlatformRepositoryServiceLocator repositoryLocator,
491
        ISelectedDataSets selectedDataSet) => new MsSqlExtractionReleasePotential(repositoryLocator, selectedDataSet);
×
492

493
    public override FixedReleaseSource<ReleaseAudit> GetReleaseSource(ICatalogueRepository catalogueRepository) =>
494
        new MsSqlReleaseSource(catalogueRepository);
×
495

496
    public override GlobalReleasePotential GetGlobalReleasabilityEvaluator(
497
        IRDMPPlatformRepositoryServiceLocator repositoryLocator, ISupplementalExtractionResults globalResult,
498
        IMapsDirectlyToDatabaseTable globalToCheck) =>
499
        new MsSqlGlobalsReleasePotential(repositoryLocator, globalResult, globalToCheck);
×
500

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

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

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

523
        dt.TableName = GetTableName(_destinationDatabase.Server.GetQuerySyntaxHelper()
4✔
524
            .GetSensibleEntityNameFromString(sqlTable.Name));
4✔
525
        linesWritten = dt.Rows.Count;
4✔
526

527
        var destinationDb = GetDestinationDatabase(listener);
4✔
528
        var tbl = destinationDb.ExpectTable(dt.TableName);
4✔
529

530
        if (tbl.Exists())
4!
531
            tbl.Drop();
×
532

533
        destinationDb.CreateTable(dt.TableName, dt);
4✔
534
        destinationDescription = $"{TargetDatabaseServer.ID}|{GetDatabaseName()}|{dt.TableName}";
4✔
535
    }
8✔
536

537

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

544
        dt.TableName = GetTableName(_destinationDatabase.Server.GetQuerySyntaxHelper()
2✔
545
            .GetSensibleEntityNameFromString(lookup.TableInfo.Name));
2✔
546

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

551
        var destinationDb = GetDestinationDatabase(listener);
2✔
552
        var existing = destinationDb.ExpectTable(dt.TableName);
2✔
553

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

561
        destinationDb.CreateTable(dt.TableName, dt);
2✔
562
    }
4✔
563

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

570
        var databaseName = GetDatabaseName();
50✔
571

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

574
        var db = discoveredServer.ExpectDatabase(databaseName);
50✔
575
        if (!db.Exists())
50✔
576
            db.Create();
10✔
577

578
        return db;
50✔
579
    }
580

581
    private string GetDatabaseName()
582
    {
583
        var dbName = DatabaseNamingPattern;
106✔
584

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

588
        if (_request == null)
106!
589
            throw new Exception("No IExtractCommand Request was passed to this component");
×
590

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

594
        dbName = dbName.Replace("$p", _project.Name)
106✔
595
            .Replace("$n", _project.ProjectNumber.ToString())
106✔
596
            .Replace("$t", _project.MasterTicket)
106✔
597
            .Replace("$r", _request.Configuration.RequestTicket)
106✔
598
            .Replace("$l", _request.Configuration.ReleaseTicket)
106✔
599
            .Replace("$e", _request.Configuration.ID.ToString());
106✔
600
        return dbName;
106✔
601
    }
602

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

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

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

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

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

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

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

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

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

667
        base.Check(notifier);
6✔
668

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

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

690
            var tables = database.DiscoverTables(false);
2✔
691

692
            if (tables.Any())
2!
693
            {
694
                string tableName;
695

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

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

© 2025 Coveralls, Inc