• 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

46.74
/Rdmp.Core/QueryBuilding/CohortSummaryQueryBuilder.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.Linq;
9
using Rdmp.Core.Curation.Data;
10
using Rdmp.Core.Curation.Data.Aggregation;
11
using Rdmp.Core.Curation.Data.Cohort;
12
using Rdmp.Core.Curation.Data.Spontaneous;
13
using Rdmp.Core.Providers;
14
using Rdmp.Core.Repositories;
15

16
namespace Rdmp.Core.QueryBuilding;
17

18
/// <summary>
19
/// Allows you to generate adjusted AggregateBuilders in which a basic AggregateBuilder from an AggregateConfiguration is adjusted to include an inception WHERE statement
20
/// which restricts the results to only those patients who are in a cohort (the cohort is the list of private identifiers returned by the AggregateConfiguration passed
21
/// into the constructor as the 'cohort' argument)
22
/// </summary>
23
public class CohortSummaryQueryBuilder
24
{
25
    private AggregateConfiguration _summary;
26

27
    private ISqlParameter[] _globals;
28
    private IColumn _extractionIdentifierColumn;
29

30
    private AggregateConfiguration _cohort;
31
    private readonly ICoreChildProvider _childProvider;
32
    private CohortAggregateContainer _cohortContainer;
33

34
    /// <summary>
35
    /// Read class description to see what the class does, use this constructor to specify an Aggregate graph and a cohort with which to restrict it.  The cohort
36
    /// aggregate must return a list of private identifiers.  The parameters must belong to the same Catalogue (dataset).
37
    /// </summary>
38
    /// <param name="summary">A basic aggregate that you want to restrict by cohort e.g. a pivot on drugs prescribed over time with an axis interval of year</param>
39
    /// <param name="cohort">A cohort aggregate that has a single AggregateDimension which must be an IsExtractionIdentifier and must follow the correct cohort aggregate naming conventions (See IsCohortIdentificationAggregate)</param>
40
    /// <param name="childProvider"></param>
41
    public CohortSummaryQueryBuilder(AggregateConfiguration summary, AggregateConfiguration cohort,
18✔
42
        ICoreChildProvider childProvider)
18✔
43
    {
44
        if (cohort == null)
18!
45
            throw new ArgumentException("cohort was null in CohortSummaryQueryBuilder constructor", nameof(cohort));
×
46

47
        if (summary.Equals(cohort))
18✔
48
            throw new ArgumentException(
2✔
49
                "Summary and Cohort should be different aggregates.  Summary should be a graphable useful aggregate while cohort should return a list of private identifiers");
2✔
50

51
        ThrowIfNotValidGraph(summary);
16✔
52

53
        try
54
        {
55
            ThrowIfNotCohort(cohort);
14✔
56
        }
12✔
57
        catch (Exception e)
2✔
58
        {
59
            throw new ArgumentException(
2✔
60
                $"The second argument to constructor CohortSummaryQueryBuilder should be a cohort identification aggregate (i.e. have a single AggregateDimension marked IsExtractionIdentifier and have a name starting with {CohortIdentificationConfiguration.CICPrefix}) but the argument you passed ('{cohort}') was NOT a cohort identification configuration aggregate",
2✔
61
                e);
2✔
62
        }
63

64
        if (summary.Catalogue_ID != cohort.Catalogue_ID)
12✔
65
            throw new ArgumentException(
2✔
66
                $"Constructor arguments to CohortSummaryQueryBuilder must belong to the same dataset (i.e. have the same underlying Catalogue), the first argument (the graphable aggregate) was called '{summary} and belonged to Catalogue ID {summary.Catalogue_ID} while the second argument (the cohort) was called '{cohort}' and belonged to Catalogue ID {cohort.Catalogue_ID}");
2✔
67

68
        _summary = summary;
10✔
69
        _cohort = cohort;
10✔
70
        _childProvider = childProvider;
10✔
71

72
        //here we take the identifier from the cohort because the dataset might have multiple identifiers e.g. birth record could have patient Id, parent Id, child Id etc.  The Aggregate will already have one of those selected and only one of them selected
73
        _extractionIdentifierColumn = _cohort.AggregateDimensions.Single(d => d.IsExtractionIdentifier);
20✔
74

75
        var cic = _cohort.GetCohortIdentificationConfigurationIfAny() ?? throw new ArgumentException(
10!
76
            $"AggregateConfiguration {_cohort} looked like a cohort but did not belong to any CohortIdentificationConfiguration");
10✔
77
        _globals = cic.GetAllParameters();
10✔
78
    }
10✔
79

80

81
    public CohortSummaryQueryBuilder(AggregateConfiguration summary, CohortAggregateContainer cohortAggregateContainer)
×
82
    {
83
        ThrowIfNotValidGraph(summary);
×
84

85
        var extractionIdentifiers = summary.Catalogue.GetAllExtractionInformation(ExtractionCategory.Any)
×
86
            .Where(e => e.IsExtractionIdentifier).ToArray();
×
87

88
        if (extractionIdentifiers.Length != 1)
×
89
            throw new Exception(
×
90
                $"Aggregate Graph '{summary} cannot be used to graph the extraction identifiers of cohort aggregate container '{cohortAggregateContainer}' because it has {extractionIdentifiers.Length} IsExtractionIdentifier columns");
×
91

92
        _extractionIdentifierColumn = extractionIdentifiers.Single();
×
93
        _summary = summary;
×
94
        _cohortContainer = cohortAggregateContainer;
×
95

96
        var cic = _cohortContainer.GetCohortIdentificationConfiguration() ?? throw new ArgumentException(
×
97
            $"CohortAggregateContainer {cohortAggregateContainer} is an orphan? it does not belong to any CohortIdentificationConfiguration");
×
98
        _globals = cic.GetAllParameters();
×
99
    }
×
100

101
    /// <summary>
102
    /// Functions in two modes
103
    /// 
104
    /// <para>WhereExtractionIdentifiersIn:
105
    /// Returns a adjusted AggregateBuilder that is based on the summary AggregateConfiguration but which has an inception WHERE statement that restricts the IsExtractionIdentifier column
106
    /// by those values returned by the Cohort query.  In order that this query doesn't become super insane we require that the Cohort be cached so that it is just a simple single
107
    /// like IFilter e.g. conceptually: WHERE CHI IN (Select CHI from IndexedExtractionIdentifierList_AggregateConfiguration5)</para>
108
    /// 
109
    /// <para>WhereRecordsIn
110
    /// Returns an adjusted AggregateBuilder that is based on the summary AggregateConfiguration but which has an root AND container which includes both the container tree of the summary
111
    /// and the cohort (resulting in a graphing of the RECORDS returned by the cohort set query instead of a master set of all those patients records - as above does).</para>
112
    /// </summary>
113
    /// <returns></returns>
114
    public AggregateBuilder GetAdjustedAggregateBuilder(CohortSummaryAdjustment adjustment,
115
        IFilter singleFilterOnly = null)
116
    {
117
        switch (adjustment)
118
        {
119
            case CohortSummaryAdjustment.WhereExtractionIdentifiersIn:
120
                if (singleFilterOnly != null)
4!
121
                    throw new NotSupportedException(
×
122
                        "You cannot graph a single IFilter with CohortSummaryAdjustment.WhereExtractionIdentifiersIn");
×
123

124
                return GetAdjustedForExtractionIdentifiersIn();
4✔
125
            case CohortSummaryAdjustment.WhereRecordsIn:
126
                return GetAdjustedForRecordsIn(singleFilterOnly);
4✔
127
            default:
128
                throw new ArgumentOutOfRangeException(nameof(adjustment));
×
129
        }
130
    }
131

132
    private AggregateBuilder GetAdjustedForRecordsIn(IFilter singleFilterOnly = null)
133
    {
134
        if (_cohort == null)
4!
135
            throw new NotSupportedException(
×
136
                "This method only works when there is a cohort aggregate, it does not work for CohortAggregateContainers");
×
137

138
        var memoryRepository = new MemoryCatalogueRepository();
4✔
139

140
        //Get a builder for creating the basic aggregate graph
141
        var summaryBuilder = _summary.GetQueryBuilder();
4✔
142

143
        //Find its root container if it has one
144
        var summaryRootContainer = summaryBuilder.RootFilterContainer;
4✔
145

146
        //work out a filter SQL that will restrict the graph generated only to the cohort
147
        var cohortRootContainer = _cohort.RootFilterContainer;
4✔
148

149
        //if we are only graphing a single filter from the Cohort
150
        if (singleFilterOnly != null)
4!
151
            cohortRootContainer = new SpontaneouslyInventedFilterContainer(memoryRepository, null,
×
152
                new[] { singleFilterOnly }, FilterContainerOperation.AND);
×
153

154
        var joinUse = _cohort.PatientIndexJoinablesUsed.SingleOrDefault();
4✔
155
        var joinTo = joinUse?.JoinableCohortAggregateConfiguration?.AggregateConfiguration;
4!
156

157
        //if there is a patient index table we must join to it
158
        if (joinUse != null)
4!
159
        {
160
            //get sql for the join table
161
            var builder = new CohortQueryBuilder(joinTo, _globals, null);
×
162
            var joinableSql = new CohortQueryBuilderDependencySql(builder.SQL, builder.ParameterManager);
×
163

164
            var helper = new CohortQueryBuilderHelper();
×
165

166
            var extractionIdentifierColumn = _summary.Catalogue.GetAllExtractionInformation(ExtractionCategory.Any)
×
167
                .Where(ei => ei.IsExtractionIdentifier).ToArray();
×
168

169
            if (extractionIdentifierColumn.Length != 1)
×
170
                throw new Exception(
×
171
                    $"Catalogue behind {_summary} must have exactly 1 IsExtractionIdentifier column but it had {extractionIdentifierColumn.Length}");
×
172

173
            CohortQueryBuilderHelper.AddJoinToBuilder(_summary, extractionIdentifierColumn[0], summaryBuilder,
×
174
                new QueryBuilderArgs(joinUse, joinTo, joinableSql, null, _globals));
×
175
        }
176

177
        //if the cohort has no WHERE SQL
178
        if (cohortRootContainer == null)
4✔
179
            return summaryBuilder; //summary can be run verbatim
2✔
180

181
        //the summary has no WHERE SQL
182
        if (summaryRootContainer == null)
2!
183
        {
184
            summaryBuilder.RootFilterContainer = cohortRootContainer; //hijack the cohorts root container
×
185
        }
186
        else
187
        {
188
            //they both have WHERE SQL
189

190
            //Create a new spontaneous container (virtual memory only container) that contains both subtrees
191
            var spontContainer = new SpontaneouslyInventedFilterContainer(memoryRepository,
2✔
192
                new[] { cohortRootContainer, summaryRootContainer }, null, FilterContainerOperation.AND);
2✔
193
            summaryBuilder.RootFilterContainer = spontContainer;
2✔
194
        }
195

196
        //better import the globals because WHERE logic from the cohort has been inherited... only problem will be if there are conflicting globals in users aggregate but that's just tough luck
197
        foreach (var p in _globals)
8✔
198
            summaryBuilder.ParameterManager.AddGlobalParameter(p);
2✔
199

200
        return summaryBuilder;
2✔
201
    }
202

203
    private AggregateBuilder GetAdjustedForExtractionIdentifiersIn()
204
    {
205
        var cachingServer = GetQueryCachingServer() ??
4!
206
                            throw new NotSupportedException("No Query Caching Server configured");
4✔
207
        var memoryRepository = new MemoryCatalogueRepository();
×
208

209
        //Get a builder for creating the basic aggregate graph
210
        var builder = _summary.GetQueryBuilder();
×
211

212
        //Find its root container if it has one
213
        var oldRootContainer = builder.RootFilterContainer;
×
214

215
        //Create a new spontaneous container (virtual memory only container, this will include an in line filter that restricts the graph to match the cohort and then include a subcontainer with the old root container - if there was one)
216
        var spontContainer = new SpontaneouslyInventedFilterContainer(memoryRepository,
×
217
            oldRootContainer != null ? new[] { oldRootContainer } : null, null, FilterContainerOperation.AND);
×
218

219
        //work out a filter SQL that will restrict the graph generated only to the cohort
220
        var cohortQueryBuilder = GetBuilder();
×
221
        cohortQueryBuilder.CacheServer = cachingServer;
×
222

223
        //It is comming direct from the cache so we don't need to output any parameters... the only ones that would appear are the globals anyway and those are not needed since cache
224
        cohortQueryBuilder.DoNotWriteOutParameters = true;
×
225
        //the basic cohort SQL select chi from dataset where ....
226
        var cohortSql = cohortQueryBuilder.SQL;
×
227

228
        if (cohortQueryBuilder.Results.CountOfCachedSubQueries == 0 || cohortQueryBuilder.Results.CountOfSubQueries !=
×
229
            cohortQueryBuilder.Results.CountOfCachedSubQueries)
×
230
            throw new NotSupportedException(
×
231
                $"Only works for 100% Cached queries, your query has {cohortQueryBuilder.Results.CountOfCachedSubQueries}/{cohortQueryBuilder.Results.CountOfSubQueries} queries cached");
×
232

233
        //there will be a single dimension on the cohort aggregate so this translates to "MyTable.MyDataset.CHI in Select(
234
        var filterSql = $"{_extractionIdentifierColumn.SelectSQL} IN ({cohortSql})";
×
235

236
        //Add a filter which restricts the graph generated to the cohort only
237
        spontContainer.AddChild(new SpontaneouslyInventedFilter(memoryRepository, spontContainer, filterSql,
×
238
            "Patient is in cohort",
×
239
            "Ensures the patients in the summary aggregate are also in the cohort aggregate (and only them)", null));
×
240

241
        builder.RootFilterContainer = spontContainer;
×
242

243
        return builder;
×
244
    }
245

246
    private ExternalDatabaseServer GetQueryCachingServer()
247
    {
248
        if (_cohort != null)
4!
249
            return _cohort.GetCohortIdentificationConfigurationIfAny().QueryCachingServer;
4✔
250

251
        return _cohortContainer != null
×
252
            ? _cohortContainer.GetCohortIdentificationConfiguration().QueryCachingServer
×
253
            : throw new NotSupportedException("Expected there to be either a _cohort or a _cohortContainer");
×
254
    }
255

256
    private CohortQueryBuilder GetBuilder()
257
    {
258
        if (_cohort != null)
×
259
            return new CohortQueryBuilder(_cohort, _globals, _childProvider);
×
260

261
        return _cohortContainer != null
×
262
            ? new CohortQueryBuilder(_cohortContainer, _globals, _childProvider)
×
263
            : throw new NotSupportedException("Expected there to be either a _cohort or a _cohortContainer");
×
264
    }
265

266
    public static AggregateConfiguration[] GetAllCompatibleSummariesForCohort(AggregateConfiguration cohort)
267
    {
268
        ThrowIfNotCohort(cohort);
×
269

270
        return cohort.Catalogue.AggregateConfigurations.Where(a => !a.IsCohortIdentificationAggregate).ToArray();
×
271
    }
272

273

274
    private static void ThrowIfNotCohort(AggregateConfiguration cohort)
275
    {
276
        if (!cohort.IsCohortIdentificationAggregate)
14✔
277
            throw new ArgumentException(
2✔
278
                $"AggregateConfiguration {cohort} was a not a cohort identification configuration aggregate its name didn't start with '{CohortIdentificationConfiguration.CICPrefix}', this is not allowed, the second argument must always be a cohort specific aggregate with only a single column marked IsExtractionIdentifier etc");
2✔
279

280
        if (cohort.AggregateDimensions.Count(d => d.IsExtractionIdentifier) != 1)
24!
281
            throw new Exception(
×
282
                $"Expected cohort {cohort} to have exactly 1 column which would be an IsExtractionIdentifier");
×
283
    }
12✔
284

285
    private static void ThrowIfNotValidGraph(AggregateConfiguration summary)
286
    {
287
        if (summary == null)
16!
288
            throw new ArgumentException("summary was null in CohortSummaryQueryBuilder constructor", nameof(summary));
×
289

290
        if (summary.IsCohortIdentificationAggregate)
16✔
291
            throw new ArgumentException(
2✔
292
                $"The first argument to constructor CohortSummaryQueryBuilder should be a basic AggregateConfiguration (i.e. not a cohort) but the argument you passed ('{summary}') was a cohort identification configuration aggregate");
2✔
293
    }
14✔
294
}
295

296
public enum CohortSummaryAdjustment
297
{
298
    WhereExtractionIdentifiersIn,
299
    WhereRecordsIn
300
}
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