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

HicServices / RDMP / 6237307473

19 Sep 2023 04:02PM UTC coverage: 57.015% (-0.4%) from 57.44%
6237307473

push

github

web-flow
Feature/rc4 (#1570)

* Syntax tidying
* Dependency updates
* Event handling singletons (ThrowImmediately and co)

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: James A Sutherland <>
Co-authored-by: James Friel <jfriel001@dundee.ac.uk>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

10734 of 20259 branches covered (0.0%)

Branch coverage included in aggregate %.

5922 of 5922 new or added lines in 565 files covered. (100.0%)

30687 of 52390 relevant lines covered (58.57%)

7361.8 hits per line

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

65.75
/Rdmp.Core/DataExport/DataExtraction/Pipeline/Destinations/ExecuteFullExtractionToDatabaseMSSql.cs
1
// Copyright (c) The University of Dundee 2018-2019
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; }
74✔
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; }
66✔
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; }
42✔
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; }
4✔
66

67
    [DemandsInitialization(DataTableUploadDestination.AlterTimeout_Description, DefaultValue = 300)]
68
    public int AlterTimeout { get; set; }
6✔
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; }
8✔
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; }
4✔
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;
18✔
84

85
    private DiscoveredDatabase _destinationDatabase;
86
    private DataTableUploadDestination _destination;
87

88
    private bool _tableDidNotExistAtStartOfLoad;
89
    private bool _isTableAlreadyNamed;
90
    private DataTable _toProcess;
91

92
    public ExecuteFullExtractionToDatabaseMSSql() : base(false)
14✔
93
    {
94
    }
14✔
95

96
    public override DataTable ProcessPipelineData(DataTable toProcess, IDataLoadEventListener job,
97
        GracefulCancellationToken cancellationToken)
98
    {
99
        _destinationDatabase = GetDestinationDatabase(job);
4✔
100
        return base.ProcessPipelineData(toProcess, job, cancellationToken);
4✔
101
    }
102

103
    protected override void Open(DataTable toProcess, IDataLoadEventListener job,
104
        GracefulCancellationToken cancellationToken)
105
    {
106
        _toProcess = toProcess;
2✔
107

108
        //give the data table the correct name
109
        if (_toProcess.ExtendedProperties.ContainsKey("ProperlyNamed") &&
2!
110
            _toProcess.ExtendedProperties["ProperlyNamed"].Equals(true))
2✔
111
            _isTableAlreadyNamed = true;
×
112

113
        _toProcess.TableName = GetTableName();
2✔
114

115
        _destination = PrepareDestination(job, _toProcess);
2✔
116
        OutputFile = _toProcess.TableName;
2✔
117
    }
2✔
118

119
    protected override void WriteRows(DataTable toProcess, IDataLoadEventListener job,
120
        GracefulCancellationToken cancellationToken, Stopwatch stopwatch)
121
    {
122
        // empty batches are allowed when using batch/resume
123
        if (toProcess.Rows.Count == 0 && _request.IsBatchResume) return;
2!
124

125
        if (_request.IsBatchResume) _destination.AllowLoadingPopulatedTables = true;
2!
126

127
        _destination.ProcessPipelineData(toProcess, job, cancellationToken);
2✔
128

129
        LinesWritten += toProcess.Rows.Count;
2✔
130
    }
2✔
131

132
    private DataTableUploadDestination PrepareDestination(IDataLoadEventListener listener, DataTable toProcess)
133
    {
134
        //see if the user has entered an extraction server/database
135
        if (TargetDatabaseServer == null)
2!
136
            throw new Exception(
×
137
                "TargetDatabaseServer (the place you want to extract the project data to) property has not been set!");
×
138

139
        try
140
        {
141
            if (!_destinationDatabase.Exists())
2!
142
                _destinationDatabase.Create();
×
143

144
            if (_request is ExtractGlobalsCommand)
2!
145
                return null;
×
146

147
            var tblName = _toProcess.TableName;
2✔
148

149
            //See if table already exists on the server (likely to cause problems including duplication, schema changes in configuration etc)
150
            var existing = _destinationDatabase.ExpectTable(tblName);
2✔
151
            if (existing.Exists())
2!
152
            {
153
                if (_request.IsBatchResume)
×
154
                {
155
                    listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Information,
×
156
                        $"Table {existing.GetFullyQualifiedName()} already exists but it IsBatchResume so no problem."));
×
157
                }
158
                else if (AlwaysDropExtractionTables)
×
159
                {
160
                    listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Warning,
×
161
                        $"Table {existing.GetFullyQualifiedName()} already exists, dropping because setting {nameof(AlwaysDropExtractionTables)} is on"));
×
162
                    existing.Drop();
×
163

164
                    listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Warning,
×
165
                        $"Table {existing.GetFullyQualifiedName()} was dropped"));
×
166

167
                    // since we dropped it we should treat it as if it was never there to begin with
168
                    _tableDidNotExistAtStartOfLoad = true;
×
169
                }
170
                else
171
                {
172
                    listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Warning,
×
173
                        $"A table called {tblName} already exists on server {TargetDatabaseServer}, data load might crash if it is populated and/or has an incompatible schema"));
×
174
                }
175
            }
176
            else
177
            {
178
                _tableDidNotExistAtStartOfLoad = true;
2✔
179
            }
180
        }
2✔
181
        catch (Exception e)
×
182
        {
183
            //Probably the database didn't exist or the credentials were wrong or something
184
            listener.OnNotify(this,
×
185
                new NotifyEventArgs(ProgressEventType.Error,
×
186
                    "Failed to inspect destination for already existing datatables", e));
×
187
        }
×
188

189
        _destination = new DataTableUploadDestination();
2✔
190

191
        PrimeDestinationTypesBasedOnCatalogueTypes(listener, toProcess);
2✔
192

193
        _destination.AllowResizingColumnsAtUploadTime = true;
2✔
194
        _destination.AlterTimeout = AlterTimeout;
2✔
195

196
        _destination.PreInitialize(_destinationDatabase, listener);
2✔
197

198
        return _destination;
2✔
199
    }
×
200

201
    private void PrimeDestinationTypesBasedOnCatalogueTypes(IDataLoadEventListener listener, DataTable toProcess)
202
    {
203
        //if the extraction is of a Catalogue
204

205
        if (_request is not IExtractDatasetCommand datasetCommand)
2!
206
            return;
×
207

208
        //for every extractable column in the Catalogue
209
        foreach (var extractionInformation in datasetCommand.ColumnsToExtract.OfType<ExtractableColumn>()
16✔
210
                     .Select(ec =>
2✔
211
                         ec.CatalogueExtractionInformation)) //.GetAllExtractionInformation(ExtractionCategory.Any))
8✔
212
        {
213
            if (extractionInformation == null)
6✔
214
                continue;
215

216
            var catItem = extractionInformation.CatalogueItem;
6✔
217

218
            //if we do not know the data type or the ei is a transform
219
            if (catItem == null)
6!
220
            {
221
                listener.OnNotify(this,
×
222
                    new NotifyEventArgs(ProgressEventType.Warning,
×
223
                        $"Did not copy Types for ExtractionInformation {extractionInformation} (ID={extractionInformation.ID}) because it had no associated CatalogueItem"));
×
224
                continue;
×
225
            }
226

227
            if (catItem.ColumnInfo == null)
6!
228
            {
229
                listener.OnNotify(this,
×
230
                    new NotifyEventArgs(ProgressEventType.Warning,
×
231
                        $"Did not copy Types for ExtractionInformation {extractionInformation} (ID={extractionInformation.ID}) because it had no associated ColumnInfo"));
×
232
                continue;
×
233
            }
234

235
            if (extractionInformation.IsProperTransform())
6✔
236
            {
237
                listener.OnNotify(this,
2✔
238
                    new NotifyEventArgs(ProgressEventType.Warning,
2✔
239
                        $"Did not copy Types for ExtractionInformation {extractionInformation} (ID={extractionInformation.ID}) because it is a Transform"));
2✔
240
                continue;
2✔
241
            }
242

243
            var destinationType = GetDestinationDatabaseType(extractionInformation);
4✔
244

245
            //Tell the destination the datatype of the ColumnInfo that underlies the ExtractionInformation (this might be changed by the ExtractionInformation e.g. as a
246
            //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)
247
            //since it will just confuse the user.  Bear in mind these data types can be degraded later by the destination
248
            var columnName = extractionInformation.Alias ?? catItem.ColumnInfo.GetRuntimeName();
4✔
249
            var addedType = _destination.AddExplicitWriteType(columnName, destinationType);
4✔
250
            addedType.IsPrimaryKey = toProcess.PrimaryKey.Any(dc => dc.ColumnName == columnName);
4✔
251

252
            //if user wants to copy collation types and the destination server is the same type as the origin server
253
            if (CopyCollations && _destinationDatabase.Server.DatabaseType == catItem.ColumnInfo.TableInfo.DatabaseType)
4!
254
                addedType.Collation = catItem.ColumnInfo.Collation;
×
255

256
            listener.OnNotify(this,
4!
257
                new NotifyEventArgs(ProgressEventType.Information,
4✔
258
                    $"Set Type for {columnName} to {destinationType} (IsPrimaryKey={(addedType.IsPrimaryKey ? "true" : "false")}) to match the source table"));
4✔
259
        }
260

261
        foreach (var sub in datasetCommand.QueryBuilder.SelectColumns.Select(static sc => sc.IColumn)
16✔
262
                     .OfType<ReleaseIdentifierSubstitution>())
2✔
263
        {
264
            var columnName = sub.GetRuntimeName();
2✔
265
            var isPk = toProcess.PrimaryKey.Any(dc => dc.ColumnName == columnName);
2✔
266

267
            var addedType = _destination.AddExplicitWriteType(columnName,
2✔
268
                datasetCommand.ExtractableCohort.GetReleaseIdentifierDataType());
2✔
269
            addedType.IsPrimaryKey = isPk;
2✔
270
            addedType.AllowNulls = !isPk;
2✔
271
        }
272
    }
2✔
273

274
    private string GetDestinationDatabaseType(ConcreteColumn col)
275
    {
276
        //Make sure we know if we are going between database types
277
        var fromDbType = _destinationDatabase.Server.DatabaseType;
4✔
278
        var toDbType = col.ColumnInfo.TableInfo.DatabaseType;
4✔
279
        if (fromDbType != toDbType)
4!
280
        {
281
            var fromSyntax = col.ColumnInfo.GetQuerySyntaxHelper();
×
282
            var toSyntax = _destinationDatabase.Server.GetQuerySyntaxHelper();
×
283

284
            var intermediate = fromSyntax.TypeTranslater.GetDataTypeRequestForSQLDBType(col.ColumnInfo.Data_type);
×
285
            return toSyntax.TypeTranslater.GetSQLDBTypeForCSharpType(intermediate);
×
286
        }
287

288
        return col.ColumnInfo.Data_type;
4✔
289
    }
290

291
    private string GetTableName(string suffix = null)
292
    {
293
        string tblName;
294
        if (_isTableAlreadyNamed)
8!
295
        {
296
            tblName = SanitizeNameForDatabase(_toProcess.TableName);
×
297

298
            if (!string.IsNullOrWhiteSpace(suffix))
×
299
                tblName += $"_{suffix}";
×
300

301
            return tblName;
×
302
        }
303

304
        tblName = TableNamingPattern;
8✔
305
        var project = _request.Configuration.Project;
8✔
306

307
        tblName = tblName.Replace("$p", project.Name);
8✔
308
        tblName = tblName.Replace("$n", project.ProjectNumber.ToString());
8✔
309
        tblName = tblName.Replace("$c", _request.Configuration.Name);
8✔
310

311
        if (_request is ExtractDatasetCommand extractDatasetCommand)
8✔
312
        {
313
            tblName = tblName.Replace("$d", extractDatasetCommand.DatasetBundle.DataSet.Catalogue.Name);
6✔
314
            tblName = tblName.Replace("$a", extractDatasetCommand.DatasetBundle.DataSet.Catalogue.Acronym);
6✔
315
        }
316

317
        if (_request is ExtractGlobalsCommand)
8✔
318
        {
319
            tblName = tblName.Replace("$d", ExtractionDirectory.GLOBALS_DATA_NAME);
2✔
320
            tblName = tblName.Replace("$a", "G");
2✔
321
        }
322

323
        var cachedGetTableNameAnswer = SanitizeNameForDatabase(tblName);
8✔
324
        if (!string.IsNullOrWhiteSpace(suffix))
8✔
325
            cachedGetTableNameAnswer += $"_{suffix}";
6✔
326

327
        return cachedGetTableNameAnswer;
8✔
328
    }
329

330
    private string SanitizeNameForDatabase(string tblName)
331
    {
332
        if (_destinationDatabase == null)
8!
333
            throw new Exception(
×
334
                "Cannot pick a TableName until we know what type of server it is going to, _server is null");
×
335

336
        //get rid of brackets and dots
337
        tblName = Regex.Replace(tblName, "[.()]", "_");
8✔
338

339
        var syntax = _destinationDatabase.Server.GetQuerySyntaxHelper();
8✔
340
        syntax.ValidateTableName(tblName);
8✔
341

342
        //otherwise, fetch and cache answer
343
        var cachedGetTableNameAnswer = syntax.GetSensibleEntityNameFromString(tblName);
8✔
344

345
        return string.IsNullOrWhiteSpace(cachedGetTableNameAnswer)
8!
346
            ? throw new Exception(
8✔
347
                $"TableNamingPattern '{TableNamingPattern}' resulted in an empty string for request '{_request}'")
8✔
348
            : cachedGetTableNameAnswer;
8✔
349
    }
350

351
    public override void Dispose(IDataLoadEventListener listener, Exception pipelineFailureExceptionIfAny)
352
    {
353
        if (_destination != null)
4✔
354
        {
355
            _destination.Dispose(listener, pipelineFailureExceptionIfAny);
2✔
356

357
            //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
358
            if (pipelineFailureExceptionIfAny != null && _tableDidNotExistAtStartOfLoad && DropTableIfLoadFails)
2!
359
                if (_destinationDatabase != null)
×
360
                {
361
                    var tbl = _destinationDatabase.ExpectTable(_toProcess.TableName);
×
362

363
                    if (tbl.Exists())
×
364
                    {
365
                        listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Warning,
×
366
                            $"DropTableIfLoadFails is true so about to drop table {tbl}"));
×
367
                        tbl.Drop();
×
368
                        listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Warning, $"Dropped table {tbl}"));
×
369
                    }
370
                }
371

372
            if (pipelineFailureExceptionIfAny == null
2!
373
                && _request.IsBatchResume
2✔
374
                && MakeFinalTableDistinctWhenBatchResuming
2✔
375
                && _destinationDatabase != null
2✔
376
                && _toProcess != null)
2✔
377
            {
378
                var tbl = _destinationDatabase.ExpectTable(_toProcess.TableName);
×
379
                if (tbl.Exists())
×
380
                    // if there is no primary key then failed batches may have introduced duplication
381
                    if (!tbl.DiscoverColumns().Any(p => p.IsPrimaryKey))
×
382
                    {
383
                        listener.OnNotify(this,
×
384
                            new NotifyEventArgs(ProgressEventType.Information,
×
385
                                $"Making {tbl} distinct in case there are duplicate rows from bad batch resumes"));
×
386
                        tbl.MakeDistinct(50000000);
×
387
                        listener.OnNotify(this,
×
388
                            new NotifyEventArgs(ProgressEventType.Information, $"Finished distincting {tbl}"));
×
389
                    }
390
            }
391
        }
392

393
        TableLoadInfo?.CloseAndArchive();
4✔
394

395
        // also close off the cumulative extraction result
396
        if (_request is ExtractDatasetCommand)
4✔
397
        {
398
            var result = ((IExtractDatasetCommand)_request).CumulativeExtractionResults;
2✔
399
            if (result != null && _toProcess != null)
2✔
400
                result.CompleteAudit(GetType(), GetDestinationDescription(), TableLoadInfo.Inserts,
2✔
401
                    _request.IsBatchResume, pipelineFailureExceptionIfAny != null);
2✔
402
        }
403
    }
4✔
404

405
    public override void Abort(IDataLoadEventListener listener)
406
    {
407
        _destination?.Abort(listener);
×
408
    }
×
409

410
    protected override void PreInitializeImpl(IExtractCommand value, IDataLoadEventListener listener)
411
    {
412
    }
14✔
413

414

415
    public override string GetDestinationDescription() => GetDestinationDescription("");
4✔
416

417
    private string GetDestinationDescription(string suffix = "")
418
    {
419
        if (_toProcess == null)
6✔
420
            return _request is ExtractGlobalsCommand
2!
421
                ? "Globals"
2✔
422
                : throw new Exception("Could not describe destination because _toProcess was null");
2✔
423

424
        var tblName = _toProcess.TableName;
4✔
425
        var dbName = GetDatabaseName();
4✔
426
        return $"{TargetDatabaseServer.ID}|{dbName}|{tblName}";
4✔
427
    }
428

429
    public static DestinationType GetDestinationType() => DestinationType.Database;
×
430

431
    public override ReleasePotential GetReleasePotential(IRDMPPlatformRepositoryServiceLocator repositoryLocator,
432
        ISelectedDataSets selectedDataSet) => new MsSqlExtractionReleasePotential(repositoryLocator, selectedDataSet);
×
433

434
    public override FixedReleaseSource<ReleaseAudit> GetReleaseSource(ICatalogueRepository catalogueRepository) =>
435
        new MsSqlReleaseSource(catalogueRepository);
×
436

437
    public override GlobalReleasePotential GetGlobalReleasabilityEvaluator(
438
        IRDMPPlatformRepositoryServiceLocator repositoryLocator, ISupplementalExtractionResults globalResult,
439
        IMapsDirectlyToDatabaseTable globalToCheck) =>
440
        new MsSqlGlobalsReleasePotential(repositoryLocator, globalResult, globalToCheck);
×
441

442
    protected override void TryExtractSupportingSQLTableImpl(SupportingSQLTable sqlTable, DirectoryInfo directory,
443
        IExtractionConfiguration configuration, IDataLoadEventListener listener, out int linesWritten,
444
        out string destinationDescription)
445
    {
446
        listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Information,
4✔
447
            $"About to download SQL for global SupportingSQL {sqlTable.SQL}"));
4✔
448
        using var con = sqlTable.GetServer().GetConnection();
4✔
449
        con.Open();
4✔
450

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

454
        using var dt = new DataTable();
4✔
455
        using (var cmd = DatabaseCommandHelper.GetCommand(sqlTable.SQL, con))
4✔
456
        using (var da = DatabaseCommandHelper.GetDataAdapter(cmd))
4✔
457
        {
458
            var sw = Stopwatch.StartNew();
4✔
459
            dt.BeginLoadData();
4✔
460
            da.Fill(dt);
4✔
461
            dt.EndLoadData();
4✔
462
        }
4✔
463

464
        dt.TableName = GetTableName(_destinationDatabase.Server.GetQuerySyntaxHelper()
4✔
465
            .GetSensibleEntityNameFromString(sqlTable.Name));
4✔
466
        linesWritten = dt.Rows.Count;
4✔
467

468
        var destinationDb = GetDestinationDatabase(listener);
4✔
469
        var tbl = destinationDb.ExpectTable(dt.TableName);
4✔
470

471
        if (tbl.Exists())
4!
472
            tbl.Drop();
×
473

474
        destinationDb.CreateTable(dt.TableName, dt);
4✔
475
        destinationDescription = $"{TargetDatabaseServer.ID}|{GetDatabaseName()}|{dt.TableName}";
4✔
476
    }
8✔
477

478

479
    protected override void TryExtractLookupTableImpl(BundledLookupTable lookup, DirectoryInfo lookupDir,
480
        IExtractionConfiguration requestConfiguration, IDataLoadEventListener listener, out int linesWritten,
481
        out string destinationDescription)
482
    {
483
        using var dt = lookup.GetDataTable();
2✔
484

485
        dt.TableName = GetTableName(_destinationDatabase.Server.GetQuerySyntaxHelper()
2✔
486
            .GetSensibleEntityNameFromString(lookup.TableInfo.Name));
2✔
487

488
        //describe the destination for the abstract base
489
        destinationDescription = $"{TargetDatabaseServer.ID}|{GetDatabaseName()}|{dt.TableName}";
2✔
490
        linesWritten = dt.Rows.Count;
2✔
491

492
        var destinationDb = GetDestinationDatabase(listener);
2✔
493
        var existing = destinationDb.ExpectTable(dt.TableName);
2✔
494

495
        if (existing.Exists())
2!
496
        {
497
            listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Warning,
×
498
                $"Dropping existing Lookup table '{existing.GetFullyQualifiedName()}'"));
×
499
            existing.Drop();
×
500
        }
501

502
        destinationDb.CreateTable(dt.TableName, dt);
2✔
503
    }
4✔
504

505
    private DiscoveredDatabase GetDestinationDatabase(IDataLoadEventListener listener)
506
    {
507
        //tell user we are about to inspect it
508
        listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Information,
10✔
509
            $"About to open connection to {TargetDatabaseServer}"));
10✔
510

511
        var databaseName = GetDatabaseName();
10✔
512

513
        var discoveredServer = DataAccessPortal.ExpectServer(TargetDatabaseServer, DataAccessContext.DataExport, false);
10✔
514

515
        var db = discoveredServer.ExpectDatabase(databaseName);
10✔
516
        if (!db.Exists())
10✔
517
            db.Create();
2✔
518

519
        return db;
10✔
520
    }
521

522
    private string GetDatabaseName()
523
    {
524
        var dbName = DatabaseNamingPattern;
26✔
525

526
        if (_project.ProjectNumber == null)
26!
527
            throw new ProjectNumberException($"Project '{_project}' must have a ProjectNumber");
×
528

529
        if (_request == null)
26!
530
            throw new Exception("No IExtractCommand Request was passed to this component");
×
531

532
        if (_request.Configuration == null)
26!
533
            throw new Exception($"Request did not specify any Configuration for Project '{_project}'");
×
534

535
        dbName = dbName.Replace("$p", _project.Name)
26✔
536
            .Replace("$n", _project.ProjectNumber.ToString())
26✔
537
            .Replace("$t", _project.MasterTicket)
26✔
538
            .Replace("$r", _request.Configuration.RequestTicket)
26✔
539
            .Replace("$l", _request.Configuration.ReleaseTicket);
26✔
540

541
        return dbName;
26✔
542
    }
543

544
    public override void Check(ICheckNotifier notifier)
545
    {
546
        if (TargetDatabaseServer == null)
10✔
547
        {
548
            notifier.OnCheckPerformed(new CheckEventArgs(
2✔
549
                "Target database server property has not been set (This component does not know where to extract data to!), " +
2✔
550
                "to fix this you must edit the pipeline and choose an ExternalDatabaseServer to extract to)",
2✔
551
                CheckResult.Fail));
2✔
552
            return;
2✔
553
        }
554

555
        if (string.IsNullOrWhiteSpace(TargetDatabaseServer.Server))
8✔
556
        {
557
            notifier.OnCheckPerformed(new CheckEventArgs("TargetDatabaseServer does not have a .Server specified",
2✔
558
                CheckResult.Fail));
2✔
559
            return;
2✔
560
        }
561

562
        if (!string.IsNullOrWhiteSpace(TargetDatabaseServer.Database))
6!
563
            notifier.OnCheckPerformed(new CheckEventArgs(
×
564
                "TargetDatabaseServer has .Database specified but this will be ignored!", CheckResult.Warning));
×
565

566
        if (string.IsNullOrWhiteSpace(TableNamingPattern))
6!
567
        {
568
            notifier.OnCheckPerformed(new CheckEventArgs(
×
569
                "You must specify TableNamingPattern, this will tell the component how to name tables it generates in the remote destination",
×
570
                CheckResult.Fail));
×
571
            return;
×
572
        }
573

574
        if (string.IsNullOrWhiteSpace(DatabaseNamingPattern))
6!
575
        {
576
            notifier.OnCheckPerformed(new CheckEventArgs(
×
577
                "You must specify DatabaseNamingPattern, this will tell the component what database to create or use in the remote destination",
×
578
                CheckResult.Fail));
×
579
            return;
×
580
        }
581

582
        if (!DatabaseNamingPattern.Contains("$p") && !DatabaseNamingPattern.Contains("$n") &&
6✔
583
            !DatabaseNamingPattern.Contains("$t") && !DatabaseNamingPattern.Contains("$r") &&
6✔
584
            !DatabaseNamingPattern.Contains("$l"))
6✔
585
            notifier.OnCheckPerformed(new CheckEventArgs(
4✔
586
                "DatabaseNamingPattern does not contain any token. The tables may be created alongside existing tables and Release would be impossible.",
4✔
587
                CheckResult.Warning));
4✔
588

589
        if (!TableNamingPattern.Contains("$d") && !TableNamingPattern.Contains("$a"))
6!
590
            notifier.OnCheckPerformed(new CheckEventArgs(
×
591
                "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",
×
592
                CheckResult.Warning));
×
593

594
        if (_request == ExtractDatasetCommand.EmptyCommand)
6!
595
        {
596
            notifier.OnCheckPerformed(new CheckEventArgs(
×
597
                "Request is ExtractDatasetCommand.EmptyCommand, will not try to connect to Database",
×
598
                CheckResult.Warning));
×
599
            return;
×
600
        }
601

602
        if (TableNamingPattern != null && TableNamingPattern.Contains("$a"))
6!
603
            if (_request is ExtractDatasetCommand dsRequest && string.IsNullOrWhiteSpace(dsRequest.Catalogue.Acronym))
×
604
                notifier.OnCheckPerformed(new CheckEventArgs(
×
605
                    $"Catalogue '{dsRequest.Catalogue}' does not have an Acronym but TableNamingPattern contains $a",
×
606
                    CheckResult.Fail));
×
607

608
        base.Check(notifier);
6✔
609

610
        try
611
        {
612
            var server = DataAccessPortal.ExpectServer(TargetDatabaseServer, DataAccessContext.DataExport, false);
6✔
613
            var database = _destinationDatabase = server.ExpectDatabase(GetDatabaseName());
6✔
614

615
            if (database.Exists())
6✔
616
            {
617
                notifier.OnCheckPerformed(
2✔
618
                    new CheckEventArgs(
2✔
619
                        $"Database {database} already exists! if an extraction has already been run you may have errors if you are re-extracting the same tables",
2✔
620
                        CheckResult.Warning));
2✔
621
            }
622
            else
623
            {
624
                notifier.OnCheckPerformed(
4✔
625
                    new CheckEventArgs(
4✔
626
                        $"Database {database} does not exist on server... it will be created at runtime",
4✔
627
                        CheckResult.Success));
4✔
628
                return;
4✔
629
            }
630

631
            var tables = database.DiscoverTables(false);
2✔
632

633
            if (tables.Any())
2!
634
            {
635
                string tableName;
636

637
                try
638
                {
639
                    tableName = GetTableName();
×
640
                }
×
641
                catch (Exception ex)
×
642
                {
643
                    notifier.OnCheckPerformed(
×
644
                        new CheckEventArgs("Could not determine table name", CheckResult.Fail, ex));
×
645
                    return;
×
646
                }
647

648
                // if the expected table exists and we are not doing a batch resume
649
                if (tables.Any(t => t.GetRuntimeName().Equals(tableName)) && !_request.IsBatchResume)
×
650
                    notifier.OnCheckPerformed(new CheckEventArgs(ErrorCodes.ExistingExtractionTableInDatabase,
×
651
                        tableName, database));
×
652
            }
653
            else
654
            {
655
                notifier.OnCheckPerformed(new CheckEventArgs($"Confirmed that database {database} is empty of tables",
2✔
656
                    CheckResult.Success));
2✔
657
            }
658
        }
2✔
659
        catch (Exception e)
×
660
        {
661
            notifier.OnCheckPerformed(new CheckEventArgs(
×
662
                $"Could not connect to TargetDatabaseServer '{TargetDatabaseServer}'", CheckResult.Fail, e));
×
663
        }
×
664
    }
6✔
665
}
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