Skip to content

Commit 85edf3c

Browse files
jdar8peartreejdarvolodymyr-korzh
authored
7002 supporting reindex of deleted resources (#7143)
* - adding initial failing test - adding support for 'includeDeletedResources' to reindexProvider and ReindexJobParameters. * - adding initial failing test - adding support for 'includeDeletedResources' to reindexProvider and ReindexJobParameters. * wip * initial commit - implementation, small refactoring, lots of tests, docs * spotless * fix 2 failing tests * fix 1 more failing test * spotless * remove todo, fixme's * changelog * changelog * changelog * apply suggestion on Batch2DaoSvcImpl Co-authored-by: volodymyr-korzh <[email protected]> * apply suggestion on Batch2DaoSvcImpl Co-authored-by: volodymyr-korzh <[email protected]> * code review suggestions * code review suggestions part 1 * review suggestion: change include deleted enums / name * review suggestion: update a couple error messages * refactor getOrCreateFirstPredicateBuilder and getOrCreateResourceTablePredicateBuilder * add null check * add tests for general search with include deleted * spotless * remove public from test * update error message in test * fix test: use default value instead of null for getbean * spotless * fix the mocks * fix failing test due to change in behaviour from review * move static methods to bottom * add nonnull for review --------- Co-authored-by: peartree <[email protected]> Co-authored-by: jdar <[email protected]> Co-authored-by: volodymyr-korzh <[email protected]>
1 parent 2d5d646 commit 85edf3c

File tree

36 files changed

+1403
-122
lines changed

36 files changed

+1403
-122
lines changed

hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ public class Constants {
220220
public static final String PARAM_CONTENT_URL = "http://hl7.org/fhir/SearchParameter/Resource-content";
221221
public static final String PARAM_COUNT = "_count";
222222
public static final String PARAM_OFFSET = "_offset";
223-
public static final String PARAM_DELETE = "_delete";
223+
public static final String PARAM_INCLUDE_DELETED = "_includeDeleted";
224224
public static final String PARAM_ELEMENTS = "_elements";
225225
public static final String PARAM_ELEMENTS_EXCLUDE_MODIFIER = ":exclude";
226226
public static final String PARAM_FORMAT = "_format";
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* #%L
3+
* HAPI FHIR - Core Library
4+
* %%
5+
* Copyright (C) 2014 - 2025 Smile CDR, Inc.
6+
* %%
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
* #L%
19+
*/
20+
package ca.uhn.fhir.rest.api;
21+
22+
import ca.uhn.fhir.i18n.Msg;
23+
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
24+
import ca.uhn.fhir.util.UrlUtil;
25+
26+
import java.util.HashMap;
27+
import java.util.Map;
28+
29+
public enum SearchIncludeDeletedEnum {
30+
31+
/**
32+
* default, search on the non-deleted resources
33+
*/
34+
NEVER("never"),
35+
36+
/**
37+
* search on the deleted resources only
38+
*/
39+
EXCLUSIVE("exclusive"),
40+
41+
/**
42+
* Search on the non-deleted resources and deleted resources.
43+
*/
44+
BOTH("both");
45+
46+
private static Map<String, SearchIncludeDeletedEnum> ourCodeToEnum;
47+
private final String myCode;
48+
49+
SearchIncludeDeletedEnum(String theCode) {
50+
myCode = theCode;
51+
}
52+
53+
public String getCode() {
54+
return myCode;
55+
}
56+
57+
public static SearchIncludeDeletedEnum fromCode(String theCode) {
58+
Map<String, SearchIncludeDeletedEnum> codeToEnum = ourCodeToEnum;
59+
if (codeToEnum == null) {
60+
codeToEnum = new HashMap<>();
61+
for (SearchIncludeDeletedEnum next : values()) {
62+
codeToEnum.put(next.getCode(), next);
63+
}
64+
ourCodeToEnum = codeToEnum;
65+
}
66+
67+
SearchIncludeDeletedEnum retVal = codeToEnum.get(theCode);
68+
if (retVal == null) {
69+
throw new InvalidRequestException(Msg.code(2741) + "Invalid \"" + Constants.PARAM_INCLUDE_DELETED
70+
+ "\" mode: " + UrlUtil.sanitizeUrlPart(theCode));
71+
}
72+
73+
return retVal;
74+
}
75+
}

hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,3 +225,5 @@ ca.uhn.fhir.jpa.interceptor.validation.RuleRequireProfileDeclaration.noMatchingP
225225
ca.uhn.fhir.jpa.interceptor.validation.RuleRequireProfileDeclaration.illegalProfile=Resource of type "{0}" must not declare conformance to profile: {1}
226226

227227
ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl.invalidUseOfSearchIdentifier=Unsupported search modifier(s): "{0}" for resource type "{1}". Valid search modifiers are: {2}
228+
229+
ca.uhn.fhir.jpa.searchparam.MatchUrlService.noResourceType = Conditional URL does not include a resource type, but includes parameters which require a resource type
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
type: add
3+
issue: 7002
4+
jira: SMILE-9203
5+
title: "Added support for reindexing deleted resources. This can be done by adding the new `_includeDeleted` parameter
6+
to the Parameters resource upon submitting the `$reindex` job.
7+
See <a href=\"https://smilecdr.com/docs/fhir_repository/search_parameter_reindexing.html#reindex-batch-job\">the reindex documentation</a> for more information."

hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/batch2/Batch2DaoSvcImpl.java

Lines changed: 80 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,14 @@
3131
import ca.uhn.fhir.jpa.api.pid.TypedResourcePid;
3232
import ca.uhn.fhir.jpa.api.pid.TypedResourceStream;
3333
import ca.uhn.fhir.jpa.api.svc.IBatch2DaoSvc;
34+
import ca.uhn.fhir.jpa.dao.ISearchBuilder;
35+
import ca.uhn.fhir.jpa.dao.SearchBuilderFactory;
3436
import ca.uhn.fhir.jpa.dao.data.IResourceLinkDao;
3537
import ca.uhn.fhir.jpa.dao.data.IResourceTableDao;
3638
import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService;
3739
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
3840
import ca.uhn.fhir.jpa.model.dao.JpaPid;
41+
import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails;
3942
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
4043
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
4144
import ca.uhn.fhir.model.primitive.IdDt;
@@ -45,16 +48,20 @@
4548
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
4649
import ca.uhn.fhir.util.DateRangeUtil;
4750
import ca.uhn.fhir.util.Logs;
51+
import ca.uhn.fhir.util.UrlUtil;
4852
import jakarta.annotation.Nonnull;
4953
import jakarta.annotation.Nullable;
5054
import org.apache.commons.lang3.StringUtils;
5155
import org.apache.commons.lang3.Validate;
5256
import org.hl7.fhir.instance.model.api.IIdType;
5357

5458
import java.util.Date;
59+
import java.util.UUID;
5560
import java.util.function.Supplier;
5661
import java.util.stream.Stream;
5762

63+
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
64+
5865
public class Batch2DaoSvcImpl implements IBatch2DaoSvc {
5966
private static final org.slf4j.Logger ourLog = Logs.getBatchTroubleshootingLog();
6067

@@ -72,6 +79,8 @@ public class Batch2DaoSvcImpl implements IBatch2DaoSvc {
7279

7380
private final PartitionSettings myPartitionSettings;
7481

82+
private final SearchBuilderFactory<JpaPid> mySearchBuilderFactory;
83+
7584
@Override
7685
public boolean isAllResourceTypeSupported() {
7786
return true;
@@ -84,20 +93,24 @@ public Batch2DaoSvcImpl(
8493
DaoRegistry theDaoRegistry,
8594
FhirContext theFhirContext,
8695
IHapiTransactionService theTransactionService,
87-
PartitionSettings thePartitionSettings) {
96+
PartitionSettings thePartitionSettings,
97+
SearchBuilderFactory<JpaPid> theSearchBuilderFactory) {
8898
myResourceTableDao = theResourceTableDao;
8999
myResourceLinkDao = theResourceLinkDao;
90100
myMatchUrlService = theMatchUrlService;
91101
myDaoRegistry = theDaoRegistry;
92102
myFhirContext = theFhirContext;
93103
myTransactionService = theTransactionService;
94104
myPartitionSettings = thePartitionSettings;
105+
mySearchBuilderFactory = theSearchBuilderFactory;
95106
}
96107

97108
@Override
98109
public IResourcePidStream fetchResourceIdStream(
99110
Date theStart, Date theEnd, RequestPartitionId theRequestPartitionId, String theUrl) {
111+
100112
if (StringUtils.isBlank(theUrl)) {
113+
// first scenario
101114
return makeStreamResult(
102115
theRequestPartitionId, () -> streamResourceIdsNoUrl(theStart, theEnd, theRequestPartitionId));
103116
} else {
@@ -116,21 +129,55 @@ private Stream<TypedResourcePid> streamResourceIdsWithUrl(
116129
Date theStart, Date theEnd, String theUrl, RequestPartitionId theRequestPartitionId) {
117130
validateUrl(theUrl);
118131

119-
SearchParameterMap searchParamMap = parseQuery(theUrl);
132+
String resourceType = UrlUtil.determineResourceTypeInResourceUrl(myFhirContext, theUrl);
133+
134+
// Search in all partitions if no partition is provided
135+
ourLog.debug("No partition id detected in request - searching all partitions");
136+
RequestPartitionId thePartitionId = defaultIfNull(theRequestPartitionId, RequestPartitionId.allPartitions());
137+
138+
SearchParameterMap searchParamMap;
139+
SystemRequestDetails request = new SystemRequestDetails();
140+
request.setRequestPartitionId(thePartitionId);
141+
142+
if (isNoResourceTypeProvidedInUrl(theUrl, resourceType)) {
143+
searchParamMap = parseQuery(theUrl, null);
144+
} else {
145+
searchParamMap = parseQuery(theUrl);
146+
}
147+
120148
searchParamMap.setLastUpdated(DateRangeUtil.narrowDateRange(searchParamMap.getLastUpdated(), theStart, theEnd));
121149

122-
String resourceType = theUrl.substring(0, theUrl.indexOf('?'));
123-
IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(resourceType);
150+
if (isNoResourceTypeProvidedInUrl(theUrl, resourceType)) {
151+
return searchForResourceIdsAndType(thePartitionId, request, searchParamMap);
152+
}
124153

125-
SystemRequestDetails request = new SystemRequestDetails().setRequestPartitionId(theRequestPartitionId);
154+
IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(resourceType);
126155

127156
return dao.searchForIdStream(searchParamMap, request, null).map(pid -> new TypedResourcePid(resourceType, pid));
128157
}
129158

130-
private static TypedResourcePid typedPidFromQueryArray(Object[] thePidTypeDateArray) {
131-
JpaPid pid = (JpaPid) thePidTypeDateArray[0];
132-
String resourceType = (String) thePidTypeDateArray[1];
133-
return new TypedResourcePid(resourceType, pid);
159+
/**
160+
* Since the resource type is not specified, query the DB for resources matching the params and return resource ID and type
161+
*
162+
* @param theRequestPartitionId the partition to search on
163+
* @param theRequestDetails the theRequestDetails details
164+
* @param theSearchParams the search params
165+
* @return Stream of typed resource pids
166+
*/
167+
private Stream<TypedResourcePid> searchForResourceIdsAndType(
168+
RequestPartitionId theRequestPartitionId,
169+
SystemRequestDetails theRequestDetails,
170+
SearchParameterMap theSearchParams) {
171+
ISearchBuilder<JpaPid> builder = mySearchBuilderFactory.newSearchBuilder(null, null);
172+
return myTransactionService
173+
.withRequest(theRequestDetails)
174+
.search(() -> builder.createQueryStream(
175+
theSearchParams,
176+
new SearchRuntimeDetails(
177+
theRequestDetails, UUID.randomUUID().toString()),
178+
theRequestDetails,
179+
theRequestPartitionId))
180+
.map(pid -> new TypedResourcePid(pid.getResourceType(), pid));
134181
}
135182

136183
@Nonnull
@@ -155,6 +202,7 @@ private Stream<TypedResourcePid> streamResourceIdsNoUrl(
155202
Date theStart, Date theEnd, RequestPartitionId theRequestPartitionId) {
156203
Integer defaultPartitionId = myPartitionSettings.getDefaultPartitionId();
157204
Stream<Object[]> rowStream;
205+
158206
if (theRequestPartitionId == null || theRequestPartitionId.isAllPartitions()) {
159207
ourLog.debug("Search for resources - all partitions");
160208
rowStream = myResourceTableDao.streamIdsTypesAndUpdateTimesOfResourcesWithinUpdatedRangeOrderedFromOldest(
@@ -184,22 +232,38 @@ public IResourcePidList fetchResourceIdsPage(
184232
return null;
185233
}
186234

187-
private static void validateUrl(@Nonnull String theUrl) {
188-
if (!theUrl.contains("?")) {
189-
throw new InternalErrorException(Msg.code(2422) + "this should never happen: URL is missing a '?'");
190-
}
191-
}
192-
193235
@Nonnull
194236
private SearchParameterMap parseQuery(String theUrl) {
195237
String resourceType = theUrl.substring(0, theUrl.indexOf('?'));
196238
RuntimeResourceDefinition def = myFhirContext.getResourceDefinition(resourceType);
197239

198-
SearchParameterMap searchParamMap = myMatchUrlService.translateMatchUrl(theUrl, def);
240+
return parseQuery(theUrl, def);
241+
}
242+
243+
@Nonnull
244+
private SearchParameterMap parseQuery(
245+
String theUrl, @Nullable RuntimeResourceDefinition theRuntimeResourceDefinition) {
246+
SearchParameterMap searchParamMap = myMatchUrlService.translateMatchUrl(theUrl, theRuntimeResourceDefinition);
199247
// this matches idx_res_type_del_updated
200248
searchParamMap.setSort(new SortSpec(Constants.PARAM_LASTUPDATED).setChain(new SortSpec(Constants.PARAM_PID)));
201249
// TODO this limits us to 2G resources.
202250
searchParamMap.setLoadSynchronousUpTo(Integer.MAX_VALUE);
203251
return searchParamMap;
204252
}
253+
254+
private static TypedResourcePid typedPidFromQueryArray(Object[] thePidTypeDateArray) {
255+
JpaPid pid = (JpaPid) thePidTypeDateArray[0];
256+
String resourceType = (String) thePidTypeDateArray[1];
257+
return new TypedResourcePid(resourceType, pid);
258+
}
259+
260+
private static void validateUrl(@Nonnull String theUrl) {
261+
if (!theUrl.contains("?")) {
262+
throw new InternalErrorException(Msg.code(2422) + "this should never happen: URL is missing a '?'");
263+
}
264+
}
265+
266+
private static boolean isNoResourceTypeProvidedInUrl(String theUrl, String resourceType) {
267+
return (resourceType == null || resourceType.isBlank()) && theUrl.indexOf('?') == 0;
268+
}
205269
}

hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/Batch2SupportConfig.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import ca.uhn.fhir.jpa.api.svc.IIdHelperService;
2828
import ca.uhn.fhir.jpa.batch2.Batch2DaoSvcImpl;
2929
import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc;
30+
import ca.uhn.fhir.jpa.dao.SearchBuilderFactory;
3031
import ca.uhn.fhir.jpa.dao.data.IResourceLinkDao;
3132
import ca.uhn.fhir.jpa.dao.data.IResourceTableDao;
3233
import ca.uhn.fhir.jpa.dao.expunge.ResourceTableFKProvider;
@@ -49,15 +50,17 @@ public IBatch2DaoSvc batch2DaoSvc(
4950
DaoRegistry theDaoRegistry,
5051
FhirContext theFhirContext,
5152
IHapiTransactionService theTransactionService,
52-
PartitionSettings thePartitionSettings) {
53+
PartitionSettings thePartitionSettings,
54+
SearchBuilderFactory theSearchBuilderFactory) {
5355
return new Batch2DaoSvcImpl(
5456
theResourceTableDao,
5557
theResourceLinkDao,
5658
theMatchUrlService,
5759
theDaoRegistry,
5860
theFhirContext,
5961
theTransactionService,
60-
thePartitionSettings);
62+
thePartitionSettings,
63+
theSearchBuilderFactory);
6164
}
6265

6366
@Bean

hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@
190190
import ca.uhn.fhir.replacereferences.ReplaceReferencesPatchBundleSvc;
191191
import ca.uhn.fhir.replacereferences.ReplaceReferencesProvenanceSvc;
192192
import ca.uhn.fhir.replacereferences.UndoReplaceReferencesSvc;
193+
import ca.uhn.fhir.rest.api.SearchIncludeDeletedEnum;
193194
import ca.uhn.fhir.rest.api.server.RequestDetails;
194195
import ca.uhn.fhir.rest.api.server.storage.IDeleteExpungeJobSubmitter;
195196
import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId;
@@ -596,8 +597,8 @@ public PersistedJpaBundleProviderFactory persistedJpaBundleProviderFactory() {
596597
}
597598

598599
@Bean
599-
public SearchBuilderFactory searchBuilderFactory() {
600-
return new SearchBuilderFactory();
600+
public SearchBuilderFactory<JpaPid> searchBuilderFactory() {
601+
return new SearchBuilderFactory<>();
601602
}
602603

603604
@Bean
@@ -725,8 +726,9 @@ public ResourceLinkPredicateBuilder newResourceLinkPredicateBuilder(
725726

726727
@Bean
727728
@Scope("prototype")
728-
public ResourceTablePredicateBuilder newResourceTablePredicateBuilder(SearchQueryBuilder theSearchBuilder) {
729-
return new ResourceTablePredicateBuilder(theSearchBuilder);
729+
public ResourceTablePredicateBuilder newResourceTablePredicateBuilder(
730+
SearchQueryBuilder theSearchBuilder, SearchIncludeDeletedEnum theSearchIncludeDeleted) {
731+
return new ResourceTablePredicateBuilder(theSearchBuilder, theSearchIncludeDeleted);
730732
}
731733

732734
@Bean

0 commit comments

Comments
 (0)