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

HicServices / RDMP / 13318987240

13 Feb 2025 11:20PM UTC coverage: 57.4% (+0.002%) from 57.398%
13318987240

push

github

jas88
Inline temporary variables

11346 of 21306 branches covered (53.25%)

Branch coverage included in aggregate %.

17 of 56 new or added lines in 7 files covered. (30.36%)

4 existing lines in 2 files now uncovered.

32210 of 54575 relevant lines covered (59.02%)

17152.02 hits per line

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

85.79
/Rdmp.Core/QueryBuilding/CohortQueryBuilderDependency.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.Collections.Concurrent;
9
using System.Collections.Generic;
10
using System.Linq;
11
using System.Threading;
12
using FAnsi.Naming;
13
using Rdmp.Core.CohortCreation.Execution;
14
using Rdmp.Core.Curation.Data;
15
using Rdmp.Core.Curation.Data.Aggregation;
16
using Rdmp.Core.Curation.Data.Cohort;
17
using Rdmp.Core.Curation.Data.Cohort.Joinables;
18
using Rdmp.Core.Providers;
19
using Rdmp.Core.QueryBuilding.Parameters;
20
using Rdmp.Core.QueryCaching.Aggregation;
21

22
namespace Rdmp.Core.QueryBuilding;
23

24
/// <summary>
25
/// A single cohort set in a <see cref="CohortIdentificationConfiguration"/> which selects specific patients from the database by their unique <see cref="IColumn.IsExtractionIdentifier"/>.
26
/// Can include a join to a patient index table.  This class stores the cached (if available) uncached and partially Cached SQL for relevant subsections of the query (and the whole query).
27
/// So that the decision about whether to use the cache can be delayed till later
28
/// 
29
/// </summary>
30
public class CohortQueryBuilderDependency
31
{
32
    private readonly IReadOnlyCollection<IPluginCohortCompiler> _pluginCohortCompilers;
33

34
    /// <summary>
35
    /// The primary table being queried
36
    /// </summary>
37
    public AggregateConfiguration CohortSet { get; }
4,164✔
38

39
    /// <summary>
40
    /// The relationship object describing the JOIN relationship between <see cref="CohortSet"/> and another optional table
41
    /// </summary>
42
    public JoinableCohortAggregateConfigurationUse PatientIndexTableIfAny { get; }
1,628✔
43

44
    /// <summary>
45
    /// The column in the <see cref="CohortSet"/> that is marked <see cref="IColumn.IsExtractionIdentifier"/>
46
    /// </summary>
47
    public AggregateDimension ExtractionIdentifierColumn { get; }
458✔
48

49
    /// <summary>
50
    /// The aggregate (query) referenced by <see cref="PatientIndexTableIfAny"/>
51
    /// </summary>
52
    public AggregateConfiguration JoinedTo { get; }
3,466✔
53

54
    /// <summary>
55
    /// The raw SQL that can be used to join the <see cref="CohortSet"/> and <see cref="PatientIndexTableIfAny"/> (if there is one).  Null if they exist
56
    /// on different servers (this is allowed only if the <see cref="CohortSet"/> is on the same server as the cache while the <see cref="PatientIndexTableIfAny"/>
57
    /// is remote).
58
    ///
59
    /// <para>This SQL does not include the parameter declaration SQL since it is designed for nesting e.g. in UNION / INTERSECT / EXCEPT hierarchy</para>
60
    /// </summary>
61
    public CohortQueryBuilderDependencySql SqlCacheless { get; private set; }
1,682✔
62

63
    /// <summary>
64
    /// The raw SQL for the <see cref="CohortSet"/> with a join against the cached artifact for the <see cref="PatientIndexTableIfAny"/>
65
    /// </summary>
66
    public CohortQueryBuilderDependencySql SqlPartiallyCached { get; private set; }
964✔
67

68
    /// <summary>
69
    /// Sql for a single cache fetch  that pulls the cached result of the <see cref="CohortSet"/> joined to <see cref="PatientIndexTableIfAny"/> (if there was any)
70
    /// </summary>
71
    public CohortQueryBuilderDependencySql SqlFullyCached { get; private set; }
2,048✔
72

73
    public CohortQueryBuilderDependencySql SqlJoinableCacheless { get; private set; }
560✔
74

75
    public CohortQueryBuilderDependencySql SqlJoinableCached { get; private set; }
552✔
76

77
    /// <summary>
78
    /// Locks on aggregate by ID
79
    /// </summary>
80
    private static readonly ConcurrentDictionary<int, object> AggregateLocks = new();
2✔
81

82

83
    public CohortQueryBuilderDependency(AggregateConfiguration cohortSet,
458✔
84
        JoinableCohortAggregateConfigurationUse patientIndexTableIfAny, ICoreChildProvider childProvider,
458✔
85
        IReadOnlyCollection<IPluginCohortCompiler> pluginCohortCompilers)
458✔
86
    {
87
        _pluginCohortCompilers = pluginCohortCompilers;
458✔
88
        CohortSet = cohortSet;
458✔
89
        PatientIndexTableIfAny = patientIndexTableIfAny;
458✔
90

91
        //record the IsExtractionIdentifier column for the log (helps with debugging count issues)
92
        var eis = cohortSet?.AggregateDimensions?.Where(static d => d.IsExtractionIdentifier).ToArray();
972!
93

94
        //Multiple IsExtractionIdentifier columns is a big problem but it's handled elsewhere
95
        if (eis is { Length: 1 })
458✔
96
            ExtractionIdentifierColumn = eis[0];
444✔
97

98
        if (PatientIndexTableIfAny == null) return;
838✔
99

100
        var join = childProvider.AllJoinables.SingleOrDefault(j =>
78!
101
                       j.ID == PatientIndexTableIfAny.JoinableCohortAggregateConfiguration_ID) ??
560✔
102
                   throw new Exception("ICoreChildProvider did not know about the provided patient index table");
78✔
103
        JoinedTo = childProvider.AllAggregateConfigurations.SingleOrDefault(ac =>
78✔
104
            ac.ID == join.AggregateConfiguration_ID);
1,704✔
105

106
        if (JoinedTo == null)
78!
NEW
107
            throw new Exception(
×
NEW
108
                "ICoreChildProvider did not know about the provided patient index table AggregateConfiguration");
×
109
    }
78✔
110

111
    public override string ToString() =>
112
        JoinedTo != null
1,112✔
113
            ? $"{CohortSet.Name}{PatientIndexTableIfAny.JoinType} JOIN {JoinedTo.Name}"
1,112✔
114
            : CohortSet.Name;
1,112✔
115

116
    public void Build(CohortQueryBuilderResult parent, ISqlParameter[] globals, CancellationToken cancellationToken)
117
    {
118
        cancellationToken.ThrowIfCancellationRequested();
458✔
119

120
        var isSolitaryPatientIndexTable = CohortSet.IsJoinablePatientIndexTable();
458✔
121

122
        // if it is a plugin aggregate we only want to ever serve up the cached SQL
123
        var pluginCohortCompiler = _pluginCohortCompilers.FirstOrDefault(c => c.ShouldRun(CohortSet));
624✔
124
        var joinedToPluginCohortCompiler =
458✔
125
            JoinedTo == null ? null : _pluginCohortCompilers.FirstOrDefault(c => c.ShouldRun(JoinedTo));
504✔
126

127
        if (pluginCohortCompiler != null)
458✔
128
        {
129
            if (joinedToPluginCohortCompiler != null)
14!
130
                throw new Exception($"APIs cannot be joined together ('{CohortSet}' and '{JoinedTo}')");
×
131

132
            if (parent.CacheManager == null)
14!
133
                throw new Exception(
×
134
                    $"Aggregate '{CohortSet}' is a plugin aggregate (According to '{pluginCohortCompiler}') but no cache is configured on {CohortSet.GetCohortIdentificationConfigurationIfAny()}.  You must enable result caching to use plugin aggregates.");
×
135

136
            // It's a plugin aggregate so only ever run the cached SQL
137
            SqlFullyCached = GetCacheFetchSqlIfPossible(parent, CohortSet, SqlCacheless, isSolitaryPatientIndexTable,
14✔
138
                pluginCohortCompiler, cancellationToken);
14✔
139

140
            if (SqlFullyCached == null)
14!
141
                throw new Exception(
×
142
                    $"Aggregate '{CohortSet}' is a plugin aggregate (According to '{pluginCohortCompiler}') but no cached results were found after running.");
×
143
            return;
14✔
144
        }
145

146
        //Includes the parameter declaration and no rename operations (i.e. couldn't be used for building the tree but can be used for cache hit testing).
147
        if (JoinedTo != null)
444✔
148
        {
149
            if (joinedToPluginCohortCompiler == null)
78✔
150
            {
151
                SqlJoinableCacheless = CohortQueryBuilderHelper.GetSQLForAggregate(JoinedTo,
74✔
152
                    new QueryBuilderArgs(
74✔
153
                        new QueryBuilderCustomArgs(), //don't respect customizations in the inception bit!
74✔
154
                        globals));
74✔
155
                SqlJoinableCached = GetCacheFetchSqlIfPossible(parent, JoinedTo, SqlJoinableCacheless, true, null,
74✔
156
                    cancellationToken);
74✔
157
            }
158
            else
159
            {
160
                // It is not possible to do a cacheless query because an API is involved
161
                SqlJoinableCached = GetCacheFetchSqlIfPossible(parent, JoinedTo, SqlJoinableCacheless, true,
4✔
162
                    joinedToPluginCohortCompiler, cancellationToken);
4✔
163

164
                // Since the only way to query the dataset is using the cache we can pretend that it is the cacheless way
165
                // of querying it too.
166
                SqlJoinableCacheless = SqlJoinableCached ?? throw new Exception(
4!
167
                    $"Unable to build query for '{CohortSet}' because it joins to API cohort '{JoinedTo}' that did not exist in the cache");
4✔
168
            }
169
        }
170

171
        if (isSolitaryPatientIndexTable)
444✔
172
        {
173
            //explicit execution of a patient index table on its own
174
            //the full uncached SQL for the query
175
            SqlCacheless =
40✔
176
                CohortQueryBuilderHelper.GetSQLForAggregate(CohortSet, new QueryBuilderArgs(parent.Customise, globals));
40✔
177

178
            if (SqlJoinableCached != null)
40!
179
                throw new QueryBuildingException("Patient index tables can't use other patient index tables!");
×
180
        }
181
        else
182
        {
183
            //the full uncached SQL for the query
184
            SqlCacheless = CohortQueryBuilderHelper.GetSQLForAggregate(CohortSet,
404✔
185
                new QueryBuilderArgs(PatientIndexTableIfAny, JoinedTo,
404✔
186
                    SqlJoinableCacheless, parent.Customise, globals));
404✔
187

188

189
            //if the joined to table is cached we can generate a partial too with full sql for the outer sql block and a cache fetch join
190
            if (SqlJoinableCached != null)
388✔
191
                SqlPartiallyCached = CohortQueryBuilderHelper.GetSQLForAggregate(CohortSet,
38✔
192
                    new QueryBuilderArgs(PatientIndexTableIfAny, JoinedTo,
38✔
193
                        SqlJoinableCached, parent.Customise, globals));
38✔
194
        }
195

196
        //We would prefer a cache hit on the exact uncached SQL
197
        SqlFullyCached = GetCacheFetchSqlIfPossible(parent, CohortSet, SqlCacheless, isSolitaryPatientIndexTable,
428✔
198
            pluginCohortCompiler, cancellationToken);
428✔
199

200
        // if we have a patient index table where the Sql is not fully cached then we should invalidate anyone using it
201
        if (isSolitaryPatientIndexTable && SqlFullyCached == null && parent.CacheManager != null)
428✔
202
            ClearCacheForUsersOfPatientIndexTable(parent.CacheManager, CohortSet);
22✔
203

204
        //but if that misses we would take a cache hit of an execution of the SqlPartiallyCached
205
        if (SqlFullyCached == null && SqlPartiallyCached != null)
428✔
206
            SqlFullyCached = GetCacheFetchSqlIfPossible(parent, CohortSet, SqlPartiallyCached,
30✔
207
                isSolitaryPatientIndexTable, pluginCohortCompiler, cancellationToken);
30✔
208
    }
428✔
209

210
    private void ClearCacheForUsersOfPatientIndexTable(CachedAggregateConfigurationResultsManager cacheManager,
211
        AggregateConfiguration cohortSet)
212
    {
213
        if (cacheManager == null)
22!
214
            return;
×
215

216
        var join = CohortSet.JoinableCohortAggregateConfiguration ?? throw new Exception(
22!
217
            $"{nameof(AggregateConfiguration.JoinableCohortAggregateConfiguration)} was null for CohortSet {cohortSet} so we were unable to clear the joinable cache users");
22✔
218

219
        // get each Aggregate Configuration that joins using this patient index table
220
        foreach (var user in join.Users.Select(j => j.AggregateConfiguration))
104✔
221
            cacheManager.DeleteCacheEntryIfAny(user, AggregateOperation.IndexedExtractionIdentifierList);
20✔
222
    }
22✔
223

224
    private CohortQueryBuilderDependencySql GetCacheFetchSqlIfPossible(CohortQueryBuilderResult parent,
225
        AggregateConfiguration aggregate,
226
        CohortQueryBuilderDependencySql sql, bool isPatientIndexTable, IPluginCohortCompiler pluginCohortCompiler,
227
        CancellationToken cancellationToken)
228
    {
229
        if (parent.CacheManager == null)
550✔
230
            return null;
316✔
231

232
        var aggregateType = isPatientIndexTable
234✔
233
            ? AggregateOperation.JoinableInceptionQuery
234✔
234
            : AggregateOperation.IndexedExtractionIdentifierList;
234✔
235
        IHasFullyQualifiedNameToo existingTable;
236

237
        // since we might make a plugin API run call we had better lock this to
238
        var oLock = AggregateLocks.GetOrAdd(aggregate.ID, i => new object());
318✔
239
        lock (oLock)
234✔
240
        {
241
            // unless it is a plugin driven aggregate we need to assemble the SQL to check if the cache is stale
242
            if (pluginCohortCompiler == null)
234✔
243
            {
244
                var parameterSql =
216✔
245
                    QueryBuilder.GetParameterDeclarationSQL(sql.ParametersUsed.Clone()
216✔
246
                        .GetFinalResolvedParametersList());
216✔
247
                var hitTestSql = parameterSql + sql.Sql;
216✔
248
                existingTable = parent.CacheManager.GetLatestResultsTable(aggregate, aggregateType, hitTestSql);
216✔
249
            }
250
            else
251
            {
252
                existingTable =
18✔
253
                    parent.CacheManager.GetLatestResultsTableUnsafe(aggregate, aggregateType, out var oldDescription);
18✔
254

255
                if (pluginCohortCompiler.IsStale(aggregate, oldDescription)) existingTable = null;
24✔
256
            }
257

258
            // if there are no cached results in the destination (and it's a plugin cohort) then we need to run the plugin API call
259
            if (existingTable == null && pluginCohortCompiler != null)
234✔
260
            {
261
                // no cached results were there so run the plugin
262
                pluginCohortCompiler.Run(CohortSet, parent.CacheManager, cancellationToken);
6✔
263

264
                // try again now
265
                existingTable = parent.CacheManager.GetLatestResultsTableUnsafe(aggregate, aggregateType);
6✔
266

267
                if (existingTable == null)
6!
268
                    throw new Exception(
×
269
                        $"Run method on {pluginCohortCompiler} failed to populate the Query Result Cache for {CohortSet}");
×
270
            }
271
        }
234✔
272

273
        //if there is a cached entry matching the cacheless SQL then we can just do a select from it (in theory)
274
        if (existingTable != null)
234✔
275
        {
276
            var sqlCachFetch =
120✔
277
                $@"{CachedAggregateConfigurationResultsManager.CachingPrefix}{aggregate.Name}*/{Environment.NewLine}select * from {existingTable.GetFullyQualifiedName()}{Environment.NewLine}";
120✔
278

279
            //Cache fetch does not require any parameters
280
            return new CohortQueryBuilderDependencySql(sqlCachFetch, new ParameterManager());
120✔
281
        }
282

283

284
        return null;
114✔
285
    }
286

287
    public string DescribeCachedState()
288
    {
289
        if (SqlFullyCached != null)
458!
290
            return "Fully Cached";
×
291

292
        if (SqlPartiallyCached != null)
458!
293
            return "Partially Cached";
×
294

295
        return SqlCacheless != null ? "Not Cached" : "Not Built";
458!
296
    }
297
}
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