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

HicServices / RDMP / 9283297040

29 May 2024 09:04AM UTC coverage: 56.516% (-0.4%) from 56.871%
9283297040

push

github

JFriel
Merge branch 'release/8.2.0' of https://github.com/HicServices/RDMP into bugfix/RDMP-153-improve-plugin-ui

10783 of 20572 branches covered (52.42%)

Branch coverage included in aggregate %.

116 of 435 new or added lines in 16 files covered. (26.67%)

373 existing lines in 24 files now uncovered.

30701 of 52830 relevant lines covered (58.11%)

7669.73 hits per line

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

67.64
/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; }
226✔
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; }
162✔
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; }
90✔
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; }
36✔
66

67
    [DemandsInitialization(DataTableUploadDestination.AlterTimeout_Description, DefaultValue = 300)]
68
    public int AlterTimeout { get; set; }
54✔
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; }
648✔
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; }
44✔
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;
82✔
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;
54✔
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;
54✔
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)
46✔
100
    {
101
    }
46✔
102

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

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

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

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

122
        _destination = PrepareDestination(job, _toProcess);
18✔
123
        OutputFile = _toProcess.TableName;
18✔
124
    }
18✔
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;
18!
131

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

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

136
        LinesWritten += toProcess.Rows.Count;
18✔
137
    }
18✔
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)
18!
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())
18!
149
                _destinationDatabase.Create();
×
150

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

154
            var tblName = _toProcess.TableName;
18✔
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);
18✔
158
            if (existing.Exists())
18✔
159
            {
160
                if (_request.IsBatchResume)
8!
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)
8!
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,
8✔
180
                        $"A table called {tblName} already exists on server {TargetDatabaseServer}, data load might crash if it is populated and/or has an incompatible schema"));
8✔
181
                }
182
            }
183
            else
184
            {
185
                _tableDidNotExistAtStartOfLoad = true;
10✔
186
            }
187
        }
18✔
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(((IExtractDatasetCommand)_request).ExtractableCohort.ExternalCohortTable);
18✔
197

198
        PrimeDestinationTypesBasedOnCatalogueTypes(listener, toProcess);
18✔
199

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

206
        return _destination;
18✔
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)
18!
214
            return;
×
215

216
        //for every extractable column in the Catalogue
217
        foreach (var extractionInformation in datasetCommand.ColumnsToExtract.OfType<ExtractableColumn>()
1,264✔
218
                     .Select(ec =>
18✔
219
                         ec.CatalogueExtractionInformation))
632✔
220
        {
221
            if (extractionInformation == null)
614✔
222
                continue;
223

224
            var catItem = extractionInformation.CatalogueItem;
614✔
225

226
            //if we do not know the data type or the ei is a transform
227
            if (catItem == null)
614!
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)
614!
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())
614✔
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);
612✔
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();
612✔
257
            var addedType = _destination.AddExplicitWriteType(columnName, destinationType);
612✔
258
            addedType.IsPrimaryKey = toProcess.PrimaryKey.Any(dc => dc.ColumnName == columnName);
1,376✔
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)
612!
262
                addedType.Collation = catItem.ColumnInfo.Collation;
×
263

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

269

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

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

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

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

297
        return col.ColumnInfo.Data_type;
612✔
298
    }
299

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

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

310
            return tblName;
×
311
        }
312

313
        tblName = TableNamingPattern;
24✔
314
        var project = _request.Configuration.Project;
24✔
315

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

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

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

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

336
        return cachedGetTableNameAnswer;
24✔
337
    }
338

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

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

348
        var syntax = _destinationDatabase.Server.GetQuerySyntaxHelper();
24✔
349
        syntax.ValidateTableName(tblName);
24✔
350

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

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

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

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

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

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

402
        TableLoadInfo?.CloseAndArchive();
36✔
403

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

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

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

423

424
    public override string GetDestinationDescription() => GetDestinationDescription("");
36✔
425

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

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

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

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

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

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

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

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

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

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

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

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

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

487

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

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

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

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

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

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

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

520
        var databaseName = GetDatabaseName();
42✔
521

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

524
        var db = discoveredServer.ExpectDatabase(databaseName);
42✔
525
        if (!db.Exists())
42✔
526
            db.Create();
8✔
527

528
        return db;
42✔
529
    }
530

531
    private string GetDatabaseName()
532
    {
533
        var dbName = DatabaseNamingPattern;
90✔
534

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

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

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

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

550
        return dbName;
90✔
551
    }
552

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

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

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

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

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

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

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

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

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

617
        base.Check(notifier);
6✔
618

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

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

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

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

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

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