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

HicServices / RDMP / 8451022258

27 Mar 2024 11:29AM UTC coverage: 56.272% (-0.6%) from 56.886%
8451022258

Pull #1779

github

web-flow
Merge branch 'develop' into task/RDMP-122-update-a-database-extraction
Pull Request #1779: Task/rdmp 122 update a database extraction

10712 of 20527 branches covered (52.18%)

Branch coverage included in aggregate %.

29 of 40 new or added lines in 3 files covered. (72.5%)

370 existing lines in 21 files now uncovered.

30518 of 52742 relevant lines covered (57.86%)

7355.92 hits per line

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

65.97
/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; }
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

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;
6✔
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;
6✔
91

92
    private DiscoveredDatabase _destinationDatabase;
93
    private DataTableUploadDestination _destination;
94

95
    private bool _tableDidNotExistAtStartOfLoad;
96
    private bool _isTableAlreadyNamed;
97
    private DataTable _toProcess;
98

99
    public ExecuteFullExtractionToDatabaseMSSql() : base(false)
14✔
100
    {
101
    }
14✔
102

103
    public override DataTable ProcessPipelineData(DataTable toProcess, IDataLoadEventListener job,
104
        GracefulCancellationToken cancellationToken)
105
    {
106
        _destinationDatabase = GetDestinationDatabase(job);
4✔
107
        return base.ProcessPipelineData(toProcess, job, cancellationToken);
4✔
108
    }
109

110
    protected override void Open(DataTable toProcess, IDataLoadEventListener job,
111
        GracefulCancellationToken cancellationToken)
112
    {
113
        _toProcess = toProcess;
2✔
114

115
        //give the data table the correct name
116
        if (_toProcess.ExtendedProperties.ContainsKey("ProperlyNamed") &&
2!
117
            _toProcess.ExtendedProperties["ProperlyNamed"].Equals(true))
2✔
118
            _isTableAlreadyNamed = true;
×
119

120
        _toProcess.TableName = GetTableName();
2✔
121

122
        _destination = PrepareDestination(job, _toProcess);
2✔
123
        OutputFile = _toProcess.TableName;
2✔
124
    }
2✔
125

126
    protected override void WriteRows(DataTable toProcess, IDataLoadEventListener job,
127
        GracefulCancellationToken cancellationToken, Stopwatch stopwatch)
128
    {
129
        // empty batches are allowed when using batch/resume
130
        if (toProcess.Rows.Count == 0 && _request.IsBatchResume) return;
2!
131

132
        if (_request.IsBatchResume) _destination.AllowLoadingPopulatedTables = true;
2!
133

134
        _destination.ProcessPipelineData(toProcess, job, cancellationToken);
2✔
135

136
        LinesWritten += toProcess.Rows.Count;
2✔
137
    }
2✔
138

139
    private DataTableUploadDestination PrepareDestination(IDataLoadEventListener listener, DataTable toProcess)
140
    {
141
        //see if the user has entered an extraction server/database
142
        if (TargetDatabaseServer == null)
2!
143
            throw new Exception(
×
144
                "TargetDatabaseServer (the place you want to extract the project data to) property has not been set!");
×
145

146
        try
147
        {
148
            if (!_destinationDatabase.Exists())
2!
149
                _destinationDatabase.Create();
×
150

151
            if (_request is ExtractGlobalsCommand)
2!
152
                return null;
×
153

154
            var tblName = _toProcess.TableName;
2✔
155

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

171
                    listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Warning,
×
172
                        $"Table {existing.GetFullyQualifiedName()} was dropped"));
×
173

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

196
        _destination = new DataTableUploadDestination();
2✔
197

198
        PrimeDestinationTypesBasedOnCatalogueTypes(listener, toProcess);
2✔
199

200
        _destination.AllowResizingColumnsAtUploadTime = true;
2✔
201
        _destination.AlterTimeout = AlterTimeout;
2✔
202
        _destination.AppendDataIfTableExists = AppendDataIfTableExists;
2✔
203
        _destination.IncludeTimeStamp = IncludeTimeStamp;
2✔
204
        _destination.PreInitialize(_destinationDatabase, listener);
2✔
205

206
        return _destination;
2✔
207
    }
×
208

209
    private void PrimeDestinationTypesBasedOnCatalogueTypes(IDataLoadEventListener listener, DataTable toProcess)
210
    {
211
        //if the extraction is of a Catalogue
212

213
        if (_request is not IExtractDatasetCommand datasetCommand)
2!
214
            return;
×
215

216
        //for every extractable column in the Catalogue
217
        foreach (var extractionInformation in datasetCommand.ColumnsToExtract.OfType<ExtractableColumn>()
16✔
218
                     .Select(ec =>
2✔
219
                         ec.CatalogueExtractionInformation))
8✔
220
        {
221
            if (extractionInformation == null)
6✔
222
                continue;
223

224
            var catItem = extractionInformation.CatalogueItem;
6✔
225

226
            //if we do not know the data type or the ei is a transform
227
            if (catItem == 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 CatalogueItem"));
×
232
                continue;
×
233
            }
234

235
            if (catItem.ColumnInfo == null)
6!
236
            {
237
                listener.OnNotify(this,
×
238
                    new NotifyEventArgs(ProgressEventType.Warning,
×
239
                        $"Did not copy Types for ExtractionInformation {extractionInformation} (ID={extractionInformation.ID}) because it had no associated ColumnInfo"));
×
240
                continue;
×
241
            }
242

243
            if (extractionInformation.IsProperTransform())
6✔
244
            {
245
                listener.OnNotify(this,
2✔
246
                    new NotifyEventArgs(ProgressEventType.Warning,
2✔
247
                        $"Did not copy Types for ExtractionInformation {extractionInformation} (ID={extractionInformation.ID}) because it is a Transform"));
2✔
248
                continue;
2✔
249
            }
250

251
            var destinationType = GetDestinationDatabaseType(extractionInformation);
4✔
252

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

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

264
            listener.OnNotify(this,
4!
265
                new NotifyEventArgs(ProgressEventType.Information,
4✔
266
                    $"Set Type for {columnName} to {destinationType} (IsPrimaryKey={(addedType.IsPrimaryKey ? "true" : "false")}) to match the source table"));
4✔
267
        }
268

269
        foreach (var sub in datasetCommand.QueryBuilder.SelectColumns.Select(static sc => sc.IColumn)
16✔
270
                     .OfType<ReleaseIdentifierSubstitution>())
2✔
271
        {
272
            var columnName = sub.GetRuntimeName();
2✔
273
            var isPk = toProcess.PrimaryKey.Any(dc => dc.ColumnName == columnName);
4✔
274

275
            var addedType = _destination.AddExplicitWriteType(columnName,
2✔
276
                datasetCommand.ExtractableCohort.GetReleaseIdentifierDataType());
2✔
277
            addedType.IsPrimaryKey = isPk;
2✔
278
            addedType.AllowNulls = !isPk;
2✔
279
        }
280
    }
2✔
281

282
    private string GetDestinationDatabaseType(ConcreteColumn col)
283
    {
284
        //Make sure we know if we are going between database types
285
        var fromDbType = _destinationDatabase.Server.DatabaseType;
4✔
286
        var toDbType = col.ColumnInfo.TableInfo.DatabaseType;
4✔
287
        if (fromDbType != toDbType)
4!
288
        {
289
            var fromSyntax = col.ColumnInfo.GetQuerySyntaxHelper();
×
290
            var toSyntax = _destinationDatabase.Server.GetQuerySyntaxHelper();
×
291

292
            var intermediate = fromSyntax.TypeTranslater.GetDataTypeRequestForSQLDBType(col.ColumnInfo.Data_type);
×
293
            return toSyntax.TypeTranslater.GetSQLDBTypeForCSharpType(intermediate);
×
294
        }
295

296
        return col.ColumnInfo.Data_type;
4✔
297
    }
298

299
    private string GetTableName(string suffix = null)
300
    {
301
        string tblName;
302
        if (_isTableAlreadyNamed)
8!
303
        {
304
            tblName = SanitizeNameForDatabase(_toProcess.TableName);
×
305

306
            if (!string.IsNullOrWhiteSpace(suffix))
×
307
                tblName += $"_{suffix}";
×
308

309
            return tblName;
×
310
        }
311

312
        tblName = TableNamingPattern;
8✔
313
        var project = _request.Configuration.Project;
8✔
314

315
        tblName = tblName.Replace("$p", project.Name);
8✔
316
        tblName = tblName.Replace("$n", project.ProjectNumber.ToString());
8✔
317
        tblName = tblName.Replace("$c", _request.Configuration.Name);
8✔
318

319
        if (_request is ExtractDatasetCommand extractDatasetCommand)
8✔
320
        {
321
            tblName = tblName.Replace("$d", extractDatasetCommand.DatasetBundle.DataSet.Catalogue.Name);
6✔
322
            tblName = tblName.Replace("$a", extractDatasetCommand.DatasetBundle.DataSet.Catalogue.Acronym);
6✔
323
        }
324

325
        if (_request is ExtractGlobalsCommand)
8✔
326
        {
327
            tblName = tblName.Replace("$d", ExtractionDirectory.GLOBALS_DATA_NAME);
2✔
328
            tblName = tblName.Replace("$a", "G");
2✔
329
        }
330

331
        var cachedGetTableNameAnswer = SanitizeNameForDatabase(tblName);
8✔
332
        if (!string.IsNullOrWhiteSpace(suffix))
8✔
333
            cachedGetTableNameAnswer += $"_{suffix}";
6✔
334

335
        return cachedGetTableNameAnswer;
8✔
336
    }
337

338
    private string SanitizeNameForDatabase(string tblName)
339
    {
340
        if (_destinationDatabase == null)
8!
341
            throw new Exception(
×
342
                "Cannot pick a TableName until we know what type of server it is going to, _server is null");
×
343

344
        //get rid of brackets and dots
345
        tblName = Regex.Replace(tblName, "[.()]", "_");
8✔
346

347
        var syntax = _destinationDatabase.Server.GetQuerySyntaxHelper();
8✔
348
        syntax.ValidateTableName(tblName);
8✔
349

350
        //otherwise, fetch and cache answer
351
        var cachedGetTableNameAnswer = syntax.GetSensibleEntityNameFromString(tblName);
8✔
352

353
        return string.IsNullOrWhiteSpace(cachedGetTableNameAnswer)
8!
354
            ? throw new Exception(
8✔
355
                $"TableNamingPattern '{TableNamingPattern}' resulted in an empty string for request '{_request}'")
8✔
356
            : cachedGetTableNameAnswer;
8✔
357
    }
358

359
    public override void Dispose(IDataLoadEventListener listener, Exception pipelineFailureExceptionIfAny)
360
    {
361
        if (_destination != null)
4✔
362
        {
363
            _destination.Dispose(listener, pipelineFailureExceptionIfAny);
2✔
364

365
            //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
366
            if (pipelineFailureExceptionIfAny != null && _tableDidNotExistAtStartOfLoad && DropTableIfLoadFails)
2!
367
                if (_destinationDatabase != null)
×
368
                {
369
                    var tbl = _destinationDatabase.ExpectTable(_toProcess.TableName);
×
370

371
                    if (tbl.Exists())
×
372
                    {
373
                        listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Warning,
×
374
                            $"DropTableIfLoadFails is true so about to drop table {tbl}"));
×
375
                        tbl.Drop();
×
376
                        listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Warning, $"Dropped table {tbl}"));
×
377
                    }
378
                }
379

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

401
        TableLoadInfo?.CloseAndArchive();
4✔
402

403
        // also close off the cumulative extraction result
404
        if (_request is ExtractDatasetCommand)
4✔
405
        {
406
            var result = ((IExtractDatasetCommand)_request).CumulativeExtractionResults;
2✔
407
            if (result != null && _toProcess != null)
2✔
408
                result.CompleteAudit(GetType(), GetDestinationDescription(), TableLoadInfo.Inserts,
2✔
409
                    _request.IsBatchResume, pipelineFailureExceptionIfAny != null);
2✔
410
        }
411
    }
4✔
412

413
    public override void Abort(IDataLoadEventListener listener)
414
    {
415
        _destination?.Abort(listener);
×
416
    }
×
417

418
    protected override void PreInitializeImpl(IExtractCommand value, IDataLoadEventListener listener)
419
    {
420
    }
14✔
421

422

423
    public override string GetDestinationDescription() => GetDestinationDescription("");
4✔
424

425
    private string GetDestinationDescription(string suffix = "")
426
    {
427
        if (_toProcess == null)
6✔
428
            return _request is ExtractGlobalsCommand
2!
429
                ? "Globals"
2✔
430
                : throw new Exception("Could not describe destination because _toProcess was null");
2✔
431

432
        var tblName = _toProcess.TableName;
4✔
433
        var dbName = GetDatabaseName();
4✔
434
        return $"{TargetDatabaseServer.ID}|{dbName}|{tblName}";
4✔
435
    }
436

437
    public static DestinationType GetDestinationType() => DestinationType.Database;
×
438

439
    public override ReleasePotential GetReleasePotential(IRDMPPlatformRepositoryServiceLocator repositoryLocator,
440
        ISelectedDataSets selectedDataSet) => new MsSqlExtractionReleasePotential(repositoryLocator, selectedDataSet);
×
441

442
    public override FixedReleaseSource<ReleaseAudit> GetReleaseSource(ICatalogueRepository catalogueRepository) =>
443
        new MsSqlReleaseSource(catalogueRepository);
×
444

445
    public override GlobalReleasePotential GetGlobalReleasabilityEvaluator(
446
        IRDMPPlatformRepositoryServiceLocator repositoryLocator, ISupplementalExtractionResults globalResult,
447
        IMapsDirectlyToDatabaseTable globalToCheck) =>
448
        new MsSqlGlobalsReleasePotential(repositoryLocator, globalResult, globalToCheck);
×
449

450
    protected override void TryExtractSupportingSQLTableImpl(SupportingSQLTable sqlTable, DirectoryInfo directory,
451
        IExtractionConfiguration configuration, IDataLoadEventListener listener, out int linesWritten,
452
        out string destinationDescription)
453
    {
454
        listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Information,
4✔
455
            $"About to download SQL for global SupportingSQL {sqlTable.SQL}"));
4✔
456
        using var con = sqlTable.GetServer().GetConnection();
4✔
457
        con.Open();
4✔
458

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

462
        using var dt = new DataTable();
4✔
463
        using (var cmd = DatabaseCommandHelper.GetCommand(sqlTable.SQL, con))
4✔
464
        using (var da = DatabaseCommandHelper.GetDataAdapter(cmd))
4✔
465
        {
466
            var sw = Stopwatch.StartNew();
4✔
467
            dt.BeginLoadData();
4✔
468
            da.Fill(dt);
4✔
469
            dt.EndLoadData();
4✔
470
        }
4✔
471

472
        dt.TableName = GetTableName(_destinationDatabase.Server.GetQuerySyntaxHelper()
4✔
473
            .GetSensibleEntityNameFromString(sqlTable.Name));
4✔
474
        linesWritten = dt.Rows.Count;
4✔
475

476
        var destinationDb = GetDestinationDatabase(listener);
4✔
477
        var tbl = destinationDb.ExpectTable(dt.TableName);
4✔
478

479
        if (tbl.Exists())
4!
480
            tbl.Drop();
×
481

482
        destinationDb.CreateTable(dt.TableName, dt);
4✔
483
        destinationDescription = $"{TargetDatabaseServer.ID}|{GetDatabaseName()}|{dt.TableName}";
4✔
484
    }
8✔
485

486

487
    protected override void TryExtractLookupTableImpl(BundledLookupTable lookup, DirectoryInfo lookupDir,
488
        IExtractionConfiguration requestConfiguration, IDataLoadEventListener listener, out int linesWritten,
489
        out string destinationDescription)
490
    {
491
        using var dt = lookup.GetDataTable();
2✔
492

493
        dt.TableName = GetTableName(_destinationDatabase.Server.GetQuerySyntaxHelper()
2✔
494
            .GetSensibleEntityNameFromString(lookup.TableInfo.Name));
2✔
495

496
        //describe the destination for the abstract base
497
        destinationDescription = $"{TargetDatabaseServer.ID}|{GetDatabaseName()}|{dt.TableName}";
2✔
498
        linesWritten = dt.Rows.Count;
2✔
499

500
        var destinationDb = GetDestinationDatabase(listener);
2✔
501
        var existing = destinationDb.ExpectTable(dt.TableName);
2✔
502

503
        if (existing.Exists())
2!
504
        {
505
            listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Warning,
×
506
                $"Dropping existing Lookup table '{existing.GetFullyQualifiedName()}'"));
×
507
            existing.Drop();
×
508
        }
509

510
        destinationDb.CreateTable(dt.TableName, dt);
2✔
511
    }
4✔
512

513
    private DiscoveredDatabase GetDestinationDatabase(IDataLoadEventListener listener)
514
    {
515
        //tell user we are about to inspect it
516
        listener.OnNotify(this, new NotifyEventArgs(ProgressEventType.Information,
10✔
517
            $"About to open connection to {TargetDatabaseServer}"));
10✔
518

519
        var databaseName = GetDatabaseName();
10✔
520

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

523
        var db = discoveredServer.ExpectDatabase(databaseName);
10✔
524
        if (!db.Exists())
10✔
525
            db.Create();
2✔
526

527
        return db;
10✔
528
    }
529

530
    private string GetDatabaseName()
531
    {
532
        var dbName = DatabaseNamingPattern;
26✔
533

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

537
        if (_request == null)
26!
538
            throw new Exception("No IExtractCommand Request was passed to this component");
×
539

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

543
        dbName = dbName.Replace("$p", _project.Name)
26✔
544
            .Replace("$n", _project.ProjectNumber.ToString())
26✔
545
            .Replace("$t", _project.MasterTicket)
26✔
546
            .Replace("$r", _request.Configuration.RequestTicket)
26✔
547
            .Replace("$l", _request.Configuration.ReleaseTicket);
26✔
548

549
        return dbName;
26✔
550
    }
551

552
    public override void Check(ICheckNotifier notifier)
553
    {
554
        if (TargetDatabaseServer == null)
10✔
555
        {
556
            notifier.OnCheckPerformed(new CheckEventArgs(
2✔
557
                "Target database server property has not been set (This component does not know where to extract data to!), " +
2✔
558
                "to fix this you must edit the pipeline and choose an ExternalDatabaseServer to extract to)",
2✔
559
                CheckResult.Fail));
2✔
560
            return;
2✔
561
        }
562

563
        if (string.IsNullOrWhiteSpace(TargetDatabaseServer.Server))
8✔
564
        {
565
            notifier.OnCheckPerformed(new CheckEventArgs("TargetDatabaseServer does not have a .Server specified",
2✔
566
                CheckResult.Fail));
2✔
567
            return;
2✔
568
        }
569

570
        if (!string.IsNullOrWhiteSpace(TargetDatabaseServer.Database))
6!
571
            notifier.OnCheckPerformed(new CheckEventArgs(
×
572
                "TargetDatabaseServer has .Database specified but this will be ignored!", CheckResult.Warning));
×
573

574
        if (string.IsNullOrWhiteSpace(TableNamingPattern))
6!
575
        {
576
            notifier.OnCheckPerformed(new CheckEventArgs(
×
577
                "You must specify TableNamingPattern, this will tell the component how to name tables it generates in the remote destination",
×
578
                CheckResult.Fail));
×
579
            return;
×
580
        }
581

582
        if (string.IsNullOrWhiteSpace(DatabaseNamingPattern))
6!
583
        {
584
            notifier.OnCheckPerformed(new CheckEventArgs(
×
585
                "You must specify DatabaseNamingPattern, this will tell the component what database to create or use in the remote destination",
×
586
                CheckResult.Fail));
×
587
            return;
×
588
        }
589

590
        if (!DatabaseNamingPattern.Contains("$p") && !DatabaseNamingPattern.Contains("$n") &&
6✔
591
            !DatabaseNamingPattern.Contains("$t") && !DatabaseNamingPattern.Contains("$r") &&
6✔
592
            !DatabaseNamingPattern.Contains("$l"))
6✔
593
            notifier.OnCheckPerformed(new CheckEventArgs(
4✔
594
                "DatabaseNamingPattern does not contain any token. The tables may be created alongside existing tables and Release would be impossible.",
4✔
595
                CheckResult.Warning));
4✔
596

597
        if (!TableNamingPattern.Contains("$d") && !TableNamingPattern.Contains("$a"))
6!
598
            notifier.OnCheckPerformed(new CheckEventArgs(
×
599
                "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",
×
600
                CheckResult.Warning));
×
601

602
        if (_request == ExtractDatasetCommand.EmptyCommand)
6!
603
        {
604
            notifier.OnCheckPerformed(new CheckEventArgs(
×
605
                "Request is ExtractDatasetCommand.EmptyCommand, will not try to connect to Database",
×
606
                CheckResult.Warning));
×
607
            return;
×
608
        }
609

610
        if (TableNamingPattern != null && TableNamingPattern.Contains("$a"))
6!
611
            if (_request is ExtractDatasetCommand dsRequest && string.IsNullOrWhiteSpace(dsRequest.Catalogue.Acronym))
×
612
                notifier.OnCheckPerformed(new CheckEventArgs(
×
613
                    $"Catalogue '{dsRequest.Catalogue}' does not have an Acronym but TableNamingPattern contains $a",
×
614
                    CheckResult.Fail));
×
615

616
        base.Check(notifier);
6✔
617

618
        try
619
        {
620
            var server = DataAccessPortal.ExpectServer(TargetDatabaseServer, DataAccessContext.DataExport, false);
6✔
621
            var database = _destinationDatabase = server.ExpectDatabase(GetDatabaseName());
6✔
622

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

639
            var tables = database.DiscoverTables(false);
2✔
640

641
            if (tables.Any())
2!
642
            {
643
                string tableName;
644

645
                try
646
                {
647
                    tableName = GetTableName();
×
648
                }
×
649
                catch (Exception ex)
×
650
                {
651
                    notifier.OnCheckPerformed(
×
652
                        new CheckEventArgs("Could not determine table name", CheckResult.Fail, ex));
×
653
                    return;
×
654
                }
655

656
                // if the expected table exists and we are not doing a batch resume or allowing data appending
NEW
657
                if (tables.Any(t => t.GetRuntimeName().Equals(tableName)) && !_request.IsBatchResume && !AppendDataIfTableExists)
×
658
                    notifier.OnCheckPerformed(new CheckEventArgs(ErrorCodes.ExistingExtractionTableInDatabase,
×
659
                        tableName, database));
×
660
            }
661
            else
662
            {
663
                notifier.OnCheckPerformed(new CheckEventArgs($"Confirmed that database {database} is empty of tables",
2✔
664
                    CheckResult.Success));
2✔
665
            }
666
        }
2✔
667
        catch (Exception e)
×
668
        {
669
            notifier.OnCheckPerformed(new CheckEventArgs(
×
670
                $"Could not connect to TargetDatabaseServer '{TargetDatabaseServer}'", CheckResult.Fail, e));
×
671
        }
×
672
    }
6✔
673
}
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