From c30973716605c4f2b6187fa951fd203af8fb2c39 Mon Sep 17 00:00:00 2001 From: James Agnew Date: Mon, 30 Nov 2020 17:59:52 -0500 Subject: [PATCH] Allow reading from multiple partitions (#2198) * Partitioning rework * Work on partition improvements * Partition updates * Work on partitiong * Test fixes * Add docs * Add changelog * Resolve FIXME * Test fixes * Test fixes * Test fixes * Compile fix * Fix compile error * Test fix * Test fixes --- .../java/ca/uhn/fhir/context/FhirContext.java | 5 +- .../interceptor/model/RequestPartitionId.java | 186 +++++-- .../ca/uhn/fhir/i18n/hapi-messages.properties | 4 +- ...llow-reading-from-multiple-partitions.yaml | 5 + .../server_jpa_partitioning/partitioning.md | 18 +- .../ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java | 3 +- .../fhir/jpa/dao/BaseHapiFhirResourceDao.java | 17 +- .../ca/uhn/fhir/jpa/dao/HistoryBuilder.java | 11 +- .../uhn/fhir/jpa/dao/LegacySearchBuilder.java | 3 +- .../uhn/fhir/jpa/dao/data/IForcedIdDao.java | 33 +- .../fhir/jpa/dao/data/IResourceTableDao.java | 24 +- .../fhir/jpa/dao/empi/EmpiLinkDeleteSvc.java | 5 +- .../dao/index/DaoResourceLinkResolver.java | 2 +- .../fhir/jpa/dao/index/IdHelperService.java | 104 ++-- ...rchParamWithInlineReferencesExtractor.java | 3 +- .../dao/predicate/BasePredicateBuilder.java | 13 +- .../dao/predicate/PredicateBuilderToken.java | 2 +- .../QueryRootEntryResourceTable.java | 4 +- .../fhir/jpa/entity/ResourceSearchView.java | 11 +- .../jpa/partition/IPartitionLookupSvc.java | 3 + .../jpa/partition/PartitionLookupSvcImpl.java | 41 +- .../partition/RequestPartitionHelperSvc.java | 166 ++++-- .../BaseJoiningPredicateBuilder.java | 16 +- .../ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java | 3 + .../jpa/dao/r4/PartitioningSqlR4Test.java | 484 ++++++++++++++---- .../PartitionSettingsSvcImplTest.java | 31 +- ...BaseMultitenantResourceProviderR4Test.java | 4 +- .../r4/BaseResourceProviderR4Test.java | 7 +- .../provider/r4/MultitenantServerR4Test.java | 63 ++- .../tasks/HapiFhirJpaMigrationTasks.java | 31 +- .../migrate/taskdef/ArbitrarySqlTaskTest.java | 3 +- .../migrate/taskdef/CalculateHashesTest.java | 17 +- .../jpa/model/entity/BasePartitionable.java | 17 +- .../BaseResourceIndexedSearchParam.java | 15 +- .../jpa/model/entity/IBaseResourceEntity.java | 4 +- .../entity/PartitionablePartitionId.java | 11 + .../ResourceHistoryProvenanceEntity.java | 1 + .../jpa/model/entity/ResourceHistoryTag.java | 71 +-- .../ResourceIndexedSearchParamQuantity.java | 10 + .../ResourceIndexedSearchParamString.java | 10 + .../ResourceIndexedSearchParamToken.java | 15 + .../entity/ResourceIndexedSearchParamUri.java | 5 + .../fhir/jpa/model/entity/ResourceTag.java | 23 +- .../jpa/model/entity/SearchParamPresent.java | 5 + .../uhn/fhir/jpa/model/util/JpaConstants.java | 5 + .../server/interceptor/auth/RuleBuilder.java | 3 +- 46 files changed, 1076 insertions(+), 441 deletions(-) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_3_0/2198-allow-reading-from-multiple-partitions.yaml diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java index 523b5ce228b..b310ef5a49d 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java @@ -470,9 +470,6 @@ public class FhirContext { /** * Returns the name of a given resource class. - * - * @param theResourceType - * @return */ public String getResourceType(final Class theResourceType) { return getResourceDefinition(theResourceType).getName(); @@ -603,7 +600,7 @@ public class FhirContext { /** * Set the restful client factory * - * @param theRestfulClientFactory + * @param theRestfulClientFactory The new client factory (must not be null) */ public void setRestfulClientFactory(final IRestfulClientFactory theRestfulClientFactory) { Validate.notNull(theRestfulClientFactory, "theRestfulClientFactory must not be null"); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/model/RequestPartitionId.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/model/RequestPartitionId.java index d66e74208d5..63720f9a24f 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/model/RequestPartitionId.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/model/RequestPartitionId.java @@ -20,12 +20,23 @@ package ca.uhn.fhir.interceptor.model; * #L% */ +import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; /** * @since 5.0.0 @@ -35,15 +46,25 @@ public class RequestPartitionId { private static final RequestPartitionId ALL_PARTITIONS = new RequestPartitionId(); private final LocalDate myPartitionDate; private final boolean myAllPartitions; - private final Integer myPartitionId; - private final String myPartitionName; + private final List myPartitionIds; + private final List myPartitionNames; /** * Constructor for a single partition */ private RequestPartitionId(@Nullable String thePartitionName, @Nullable Integer thePartitionId, @Nullable LocalDate thePartitionDate) { - myPartitionId = thePartitionId; - myPartitionName = thePartitionName; + myPartitionIds = toListOrNull(thePartitionId); + myPartitionNames = toListOrNull(thePartitionName); + myPartitionDate = thePartitionDate; + myAllPartitions = false; + } + + /** + * Constructor for a multiple partition + */ + private RequestPartitionId(@Nullable List thePartitionName, @Nullable List thePartitionId, @Nullable LocalDate thePartitionDate) { + myPartitionIds = toListOrNull(thePartitionId); + myPartitionNames = toListOrNull(thePartitionName); myPartitionDate = thePartitionDate; myAllPartitions = false; } @@ -54,8 +75,8 @@ public class RequestPartitionId { private RequestPartitionId() { super(); myPartitionDate = null; - myPartitionName = null; - myPartitionId = null; + myPartitionNames = null; + myPartitionIds = null; myAllPartitions = true; } @@ -69,28 +90,26 @@ public class RequestPartitionId { } @Nullable - public String getPartitionName() { - return myPartitionName; + public List getPartitionNames() { + return myPartitionNames; } - @Nullable - public Integer getPartitionId() { - return myPartitionId; + @Nonnull + public List getPartitionIds() { + Validate.notNull(myPartitionIds, "Partition IDs have not been set"); + return myPartitionIds; } @Override public String toString() { - return "RequestPartitionId[id=" + getPartitionId() + ", name=" + getPartitionName() + "]"; - } - - /** - * Returns the partition ID (numeric) as a string, or the string "null" - */ - public String getPartitionIdStringOrNullString() { - if (myPartitionId == null) { - return "null"; + ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE); + if (hasPartitionIds()) { + b.append("ids", getPartitionIds()); } - return myPartitionId.toString(); + if (hasPartitionNames()) { + b.append("names", getPartitionNames()); + } + return b.build(); } @Override @@ -108,8 +127,8 @@ public class RequestPartitionId { return new EqualsBuilder() .append(myAllPartitions, that.myAllPartitions) .append(myPartitionDate, that.myPartitionDate) - .append(myPartitionId, that.myPartitionId) - .append(myPartitionName, that.myPartitionName) + .append(myPartitionIds, that.myPartitionIds) + .append(myPartitionNames, that.myPartitionNames) .isEquals(); } @@ -118,11 +137,82 @@ public class RequestPartitionId { return new HashCodeBuilder(17, 37) .append(myPartitionDate) .append(myAllPartitions) - .append(myPartitionId) - .append(myPartitionName) + .append(myPartitionIds) + .append(myPartitionNames) .toHashCode(); } + @Nullable + public Integer getFirstPartitionIdOrNull() { + if (myPartitionIds != null) { + return myPartitionIds.get(0); + } + return null; + } + + public String getFirstPartitionNameOrNull() { + if (myPartitionNames != null) { + return myPartitionNames.get(0); + } + return null; + } + + /** + * Returns true if this request partition contains only one partition ID and it is the DEFAULT partition ID (null) + */ + public boolean isDefaultPartition() { + return getPartitionIds().size() == 1 && getPartitionIds().get(0) == null; + } + + public boolean hasPartitionId(Integer thePartitionId) { + Validate.notNull(myPartitionIds, "Partition IDs not set"); + return myPartitionIds.contains(thePartitionId); + } + + public boolean hasPartitionIds() { + return myPartitionIds != null; + } + + public boolean hasPartitionNames() { + return myPartitionNames != null; + } + + public boolean hasDefaultPartitionId() { + return getPartitionIds().contains(null); + } + + public List getPartitionIdsWithoutDefault() { + return getPartitionIds().stream().filter(t -> t != null).collect(Collectors.toList()); + } + + @Nullable + private static List toListOrNull(@Nullable Collection theList) { + if (theList != null) { + if (theList.size() == 1) { + return Collections.singletonList(theList.iterator().next()); + } + return Collections.unmodifiableList(new ArrayList<>(theList)); + } + return null; + } + + @Nullable + private static List toListOrNull(@Nullable T theObject) { + if (theObject != null) { + return Collections.singletonList(theObject); + } + return null; + } + + @SafeVarargs + @Nullable + private static List toListOrNull(@Nullable T... theObject) { + if (theObject != null) { + return Arrays.asList(theObject); + } + return null; + } + @Nonnull public static RequestPartitionId allPartitions() { return ALL_PARTITIONS; @@ -130,17 +220,27 @@ public class RequestPartitionId { @Nonnull public static RequestPartitionId defaultPartition() { - return fromPartitionId(null); + return fromPartitionIds(Collections.singletonList(null)); } @Nonnull public static RequestPartitionId fromPartitionId(@Nullable Integer thePartitionId) { - return fromPartitionId(thePartitionId, null); + return fromPartitionIds(Collections.singletonList(thePartitionId)); } @Nonnull public static RequestPartitionId fromPartitionId(@Nullable Integer thePartitionId, @Nullable LocalDate thePartitionDate) { - return new RequestPartitionId(null, thePartitionId, thePartitionDate); + return new RequestPartitionId(null, Collections.singletonList(thePartitionId), thePartitionDate); + } + + @Nonnull + public static RequestPartitionId fromPartitionIds(@Nonnull Collection thePartitionIds) { + return new RequestPartitionId(null, toListOrNull(thePartitionIds), null); + } + + @Nonnull + public static RequestPartitionId fromPartitionIds(Integer... thePartitionIds) { + return new RequestPartitionId(null, toListOrNull(thePartitionIds), null); } @Nonnull @@ -153,6 +253,16 @@ public class RequestPartitionId { return new RequestPartitionId(thePartitionName, null, thePartitionDate); } + @Nonnull + public static RequestPartitionId fromPartitionNames(@Nullable List thePartitionNames) { + return new RequestPartitionId(toListOrNull(thePartitionNames), null, null); + } + + @Nonnull + public static RequestPartitionId fromPartitionNames(String... thePartitionNames) { + return new RequestPartitionId(toListOrNull(thePartitionNames), null, null); + } + @Nonnull public static RequestPartitionId fromPartitionIdAndName(@Nullable Integer thePartitionId, @Nullable String thePartitionName) { return new RequestPartitionId(thePartitionName, thePartitionId, null); @@ -163,13 +273,25 @@ public class RequestPartitionId { return new RequestPartitionId(thePartitionName, thePartitionId, thePartitionDate); } + @Nonnull + public static RequestPartitionId forPartitionIdsAndNames(List thePartitionNames, List thePartitionIds, LocalDate thePartitionDate) { + return new RequestPartitionId(thePartitionNames, thePartitionIds, thePartitionDate); + } + /** * Create a string representation suitable for use as a cache key. Null aware. + *

+ * Returns the partition IDs (numeric) as a joined string with a space between, using the string "null" for any null values */ - public static String stringifyForKey(RequestPartitionId theRequestPartitionId) { - String retVal = "(null)"; - if (theRequestPartitionId != null) { - retVal = theRequestPartitionId.getPartitionIdStringOrNullString(); + public static String stringifyForKey(@Nonnull RequestPartitionId theRequestPartitionId) { + String retVal = "(all partitions)"; + if (!theRequestPartitionId.isAllPartitions()) { + assert theRequestPartitionId.hasPartitionIds(); + retVal = theRequestPartitionId + .getPartitionIds() + .stream() + .map(t -> defaultIfNull(t, "null").toString()) + .collect(Collectors.joining(" ")); } return retVal; } diff --git a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties index 3061efca231..aa90487d8f5 100644 --- a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties +++ b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties @@ -171,12 +171,10 @@ ca.uhn.fhir.jpa.dao.index.IdHelperService.nonUniqueForcedId=Non-unique ID specif ca.uhn.fhir.jpa.partition.PartitionLookupSvcImpl.noIdSupplied=No Partition ID supplied ca.uhn.fhir.jpa.partition.PartitionLookupSvcImpl.missingPartitionIdOrName=Partition must have an ID and a Name -ca.uhn.fhir.jpa.partition.PartitionLookupSvcImpl.cantCreatePartition0=Can not create a partition with ID 0 (this is a reserved value) ca.uhn.fhir.jpa.partition.PartitionLookupSvcImpl.unknownPartitionId=No partition exists with ID {0} ca.uhn.fhir.jpa.partition.PartitionLookupSvcImpl.invalidName=Partition name "{0}" is not valid ca.uhn.fhir.jpa.partition.PartitionLookupSvcImpl.cantCreateDuplicatePartitionName=Partition name "{0}" is already defined -ca.uhn.fhir.jpa.partition.PartitionLookupSvcImpl.cantDeleteDefaultPartition=Can not delete default partition -ca.uhn.fhir.jpa.partition.PartitionLookupSvcImpl.cantRenameDefaultPartition=Can not rename default partition +ca.uhn.fhir.jpa.partition.PartitionLookupSvcImpl.cantCreateDefaultPartition=Can not create partition with name "DEFAULT" ca.uhn.fhir.rest.server.interceptor.partition.RequestTenantPartitionInterceptor.unknownTenantName=Unknown tenant: {0} diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_3_0/2198-allow-reading-from-multiple-partitions.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_3_0/2198-allow-reading-from-multiple-partitions.yaml new file mode 100644 index 00000000000..6e933237a82 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_3_0/2198-allow-reading-from-multiple-partitions.yaml @@ -0,0 +1,5 @@ +--- +type: add +issue: 2198 +title: "It is now possible for read operations (read/history/search/etc) in a partitioned server to read across more than one + partition if the partitioning interceptor indicates multiple partitions." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_partitioning/partitioning.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_partitioning/partitioning.md index dd9b19f3417..395dfca3b81 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_partitioning/partitioning.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_partitioning/partitioning.md @@ -43,10 +43,23 @@ When a resource is **updated**, the partition ID and date from the previous vers When a **read operation** is being performed (e.g. a read, search, history, etc.), a separate [interceptor hook](#partition-interceptors) is invoked in order to determine whether the operation should target a specific partition. The outcome of this hook determines how the partitioning manifests itself to the end user: -* The system can be configured to operate as a **multitenant** solution by configuring the partition interceptor to scope all read operations to read data only from the partition that request has access to.``` +* The system can be configured to operate as a **multitenant** solution by configuring the partition interceptor to scope all read operations to read data only from the partition that request has access to. * The system can be configured to operate with logical segments by configuring the partition interceptor to scope read operations to access all partitions. +# Partitioning and Resource IDs + +In a partitioned repository, it is important to understand that only a single pool of resource IDs exists. In other words, only one resource with the ID `Patient/1` can exist across all partitions, and it must be in a single partition. + +This fact can have security implications: + +* A client might be blocked from creating `Patient/ABC` in the partition they have access to because this ID is already in use in another partition. + +* In a server using the default configuration of SEQUENTIAL_NUMERIC [Server ID Strategy](/hapi-fhir/apidocs/hapi-fhir-jpaserver-api/ca/uhn/fhir/jpa/api/config/DaoConfig.html#setResourceServerIdStrategy(ca.uhn.fhir.jpa.api.config.DaoConfig.IdStrategyEnum)) a client may be able to infer the IDs of resources in other partitions based on the ID they were assigned. + +These considerations can be addressed by using UUID Server ID Strategy, and disallowing client-assigned IDs. + + # Partition Interceptors In order to implement partitioning, an interceptor must be registered against the interceptor registry (either the REST Server registry, or the JPA Server registry will work). @@ -67,6 +80,9 @@ The criteria for determining the partition will depend on your use case. For exa A hook against the [`Pointcut.STORAGE_PARTITION_IDENTIFY_READ`](/hapi-fhir/apidocs/hapi-fhir-base/ca/uhn/fhir/interceptor/api/Pointcut.html#STORAGE_PARTITION_IDENTIFY_READ) pointcut must be registered, and this hook method will be invoked every time a resource is created in order to determine the partition to assign the resource to. + +As of HAPI FHIR 5.3.0, the *Identify Partition for Read* hook method may return multiple partition names or IDs. If more than one partition is identified, the server will search in all identified partitions. + ## Examples See [Partition Interceptor Examples](./partition_interceptor_examples.html) for various samples of how partitioning interceptors can be set up. diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java index ca5ceacce22..a03b6a8e532 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java @@ -37,6 +37,7 @@ import ca.uhn.fhir.jpa.model.entity.BaseHasResource; import ca.uhn.fhir.jpa.model.entity.BaseTag; import ca.uhn.fhir.jpa.model.entity.ForcedId; import ca.uhn.fhir.jpa.model.entity.IBaseResourceEntity; +import ca.uhn.fhir.jpa.model.entity.PartitionablePartitionId; import ca.uhn.fhir.jpa.model.entity.ResourceEncodingEnum; import ca.uhn.fhir.jpa.model.entity.ResourceHistoryProvenanceEntity; import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable; @@ -962,7 +963,7 @@ public abstract class BaseHapiFhirDao extends BaseStora // 7. Add partition information if (myPartitionSettings.isPartitioningEnabled()) { - RequestPartitionId partitionId = theEntity.getPartitionId(); + PartitionablePartitionId partitionId = theEntity.getPartitionId(); if (partitionId != null && partitionId.getPartitionId() != null) { PartitionEntity persistedPartition = myPartitionLookupSvc.getPartitionById(partitionId.getPartitionId()); retVal.setUserData(Constants.RESOURCE_PARTITION_ID, persistedPartition.toRequestPartitionId()); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java index e5701c53132..e8d196285b2 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java @@ -1102,19 +1102,17 @@ public abstract class BaseHapiFhirResourceDao extends B // Verify that the resource is for the correct partition if (!requestPartitionId.isAllPartitions()) { - if (requestPartitionId.getPartitionId() == null) { - if (entity.getPartitionId().getPartitionId() != null) { - ourLog.debug("Performing a read for PartitionId={} but entity has partition: {}", requestPartitionId, entity.getPartitionId()); - entity = null; - } - } else if (entity.getPartitionId().getPartitionId() != null) { - if (!requestPartitionId.getPartitionId().equals(entity.getPartitionId().getPartitionId())) { + if (entity.getPartitionId() != null && entity.getPartitionId().getPartitionId() != null) { + if (!requestPartitionId.hasPartitionId(entity.getPartitionId().getPartitionId())) { ourLog.debug("Performing a read for PartitionId={} but entity has partition: {}", requestPartitionId, entity.getPartitionId()); entity = null; } } else { - ourLog.debug("Performing a read for PartitionId=null but entity has partition: {}", entity.getPartitionId()); - entity = null; + // Entity Partition ID is null + if (!requestPartitionId.hasPartitionId(null)) { + ourLog.debug("Performing a read for PartitionId=null but entity has partition: {}", entity.getPartitionId()); + entity = null; + } } } @@ -1145,6 +1143,7 @@ public abstract class BaseHapiFhirResourceDao extends B } } + Validate.notNull(entity); validateResourceType(entity); if (theCheckForForcedId) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/HistoryBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/HistoryBuilder.java index 8ee4a385ea5..45538909fee 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/HistoryBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/HistoryBuilder.java @@ -142,10 +142,15 @@ public class HistoryBuilder { List predicates = new ArrayList<>(); if (!thePartitionId.isAllPartitions()) { - if (thePartitionId.getPartitionId() != null) { - predicates.add(theCriteriaBuilder.equal(theFrom.get("myPartitionIdValue").as(Integer.class), thePartitionId.getPartitionId())); - } else { + if (thePartitionId.isDefaultPartition()) { predicates.add(theCriteriaBuilder.isNull(theFrom.get("myPartitionIdValue").as(Integer.class))); + } else if (thePartitionId.hasDefaultPartitionId()) { + predicates.add(theCriteriaBuilder.or( + theCriteriaBuilder.isNull(theFrom.get("myPartitionIdValue").as(Integer.class)), + theFrom.get("myPartitionIdValue").as(Integer.class).in(thePartitionId.getPartitionIdsWithoutDefault()) + )); + } else { + predicates.add(theFrom.get("myPartitionIdValue").as(Integer.class).in(thePartitionId.getPartitionIds())); } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/LegacySearchBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/LegacySearchBuilder.java index 50923692f72..143dff02cf6 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/LegacySearchBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/LegacySearchBuilder.java @@ -42,6 +42,7 @@ import ca.uhn.fhir.jpa.entity.ResourceSearchView; import ca.uhn.fhir.jpa.interceptor.JpaPreResourceAccessDetails; import ca.uhn.fhir.jpa.model.config.PartitionSettings; import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam; +import ca.uhn.fhir.jpa.model.entity.PartitionablePartitionId; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedCompositeStringUnique; import ca.uhn.fhir.jpa.model.entity.ResourceLink; import ca.uhn.fhir.jpa.model.entity.ResourceTable; @@ -953,7 +954,7 @@ public class LegacySearchBuilder implements ISearchBuilder { From join = myQueryStack.createJoin(SearchBuilderJoinEnum.COMPOSITE_UNIQUE, null); if (!theRequestPartitionId.isAllPartitions()) { - Integer partitionId = theRequestPartitionId.getPartitionId(); + Integer partitionId = theRequestPartitionId.getFirstPartitionIdOrNull(); Predicate predicate = myCriteriaBuilder.equal(join.get("myPartitionIdValue").as(Integer.class), partitionId); myQueryStack.addPredicate(predicate); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IForcedIdDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IForcedIdDao.java index a64c375adcb..dddb3fe2e2e 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IForcedIdDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IForcedIdDao.java @@ -44,8 +44,11 @@ public interface IForcedIdDao extends JpaRepository { @Query("SELECT f.myResourcePid FROM ForcedId f WHERE myPartitionId.myPartitionId IS NULL AND myResourceType = :resource_type AND myForcedId = :forced_id") Optional findByPartitionIdNullAndTypeAndForcedId(@Param("resource_type") String theResourceType, @Param("forced_id") String theForcedId); - @Query("SELECT f.myResourcePid FROM ForcedId f WHERE myPartitionId.myPartitionId = :partition_id AND myResourceType = :resource_type AND myForcedId = :forced_id") - Optional findByPartitionIdAndTypeAndForcedId(@Param("partition_id") Integer thePartitionId, @Param("resource_type") String theResourceType, @Param("forced_id") String theForcedId); + @Query("SELECT f.myResourcePid FROM ForcedId f WHERE myPartitionId.myPartitionId IN :partition_id AND myResourceType = :resource_type AND myForcedId = :forced_id") + Optional findByPartitionIdAndTypeAndForcedId(@Param("partition_id") Collection thePartitionId, @Param("resource_type") String theResourceType, @Param("forced_id") String theForcedId); + + @Query("SELECT f.myResourcePid FROM ForcedId f WHERE (myPartitionId.myPartitionId IN :partition_id OR myPartitionId.myPartitionId IS NULL) AND myResourceType = :resource_type AND myForcedId = :forced_id") + Optional findByPartitionIdOrNullAndTypeAndForcedId(@Param("partition_id") Collection thePartitionId, @Param("resource_type") String theResourceType, @Param("forced_id") String theForcedId); @Query("SELECT f FROM ForcedId f WHERE f.myResourcePid = :resource_pid") Optional findByResourcePid(@Param("resource_pid") Long theResourcePid); @@ -65,8 +68,15 @@ public interface IForcedIdDao extends JpaRepository { * This method returns a Collection where each row is an element in the collection. Each element in the collection * is an object array, where the order matters (the array represents columns returned by the query). Be careful if you change this query in any way. */ - @Query("SELECT f.myForcedId, f.myResourcePid FROM ForcedId f WHERE myPartitionIdValue = :partition_id AND myResourceType = :resource_type AND myForcedId IN ( :forced_id )") - Collection findByTypeAndForcedIdInPartition(@Param("resource_type") String theResourceType, @Param("forced_id") Collection theForcedId, @Param("partition_id") Integer thePartitionId); + @Query("SELECT f.myForcedId, f.myResourcePid FROM ForcedId f WHERE myPartitionIdValue IN ( :partition_id ) AND myResourceType = :resource_type AND myForcedId IN ( :forced_id )") + Collection findByTypeAndForcedIdInPartitionIds(@Param("resource_type") String theResourceType, @Param("forced_id") Collection theForcedId, @Param("partition_id") Collection thePartitionId); + + /** + * This method returns a Collection where each row is an element in the collection. Each element in the collection + * is an object array, where the order matters (the array represents columns returned by the query). Be careful if you change this query in any way. + */ + @Query("SELECT f.myForcedId, f.myResourcePid FROM ForcedId f WHERE (myPartitionIdValue IS NULL OR myPartitionIdValue IN ( :partition_id )) AND myResourceType = :resource_type AND myForcedId IN ( :forced_id )") + Collection findByTypeAndForcedIdInPartitionIdsOrNullPartition(@Param("resource_type") String theResourceType, @Param("forced_id") Collection theForcedId, @Param("partition_id") Collection thePartitionId); /** * This method returns a Collection where each row is an element in the collection. Each element in the collection @@ -110,8 +120,8 @@ public interface IForcedIdDao extends JpaRepository { " f.myResourceType, f.myResourcePid, f.myForcedId, t.myDeleted " + "FROM ForcedId f " + "JOIN ResourceTable t ON t.myId = f.myResourcePid " + - "WHERE f.myResourceType = :resource_type AND f.myForcedId IN ( :forced_id ) AND f.myPartitionIdValue = :partition_id") - Collection findAndResolveByForcedIdWithNoTypeInPartition(@Param("resource_type") String theResourceType, @Param("forced_id") Collection theForcedIds, @Param("partition_id") Integer thePartitionId); + "WHERE f.myResourceType = :resource_type AND f.myForcedId IN ( :forced_id ) AND f.myPartitionIdValue IN :partition_id") + Collection findAndResolveByForcedIdWithNoTypeInPartition(@Param("resource_type") String theResourceType, @Param("forced_id") Collection theForcedIds, @Param("partition_id") Collection thePartitionId); /** @@ -127,4 +137,15 @@ public interface IForcedIdDao extends JpaRepository { Collection findAndResolveByForcedIdWithNoTypeInPartitionNull(@Param("resource_type") String theResourceType, @Param("forced_id") Collection theForcedIds); + /** + * This method returns a Collection where each row is an element in the collection. Each element in the collection + * is an object array, where the order matters (the array represents columns returned by the query). Be careful if you change this query in any way. + */ + @Query("" + + "SELECT " + + " f.myResourceType, f.myResourcePid, f.myForcedId, t.myDeleted " + + "FROM ForcedId f " + + "JOIN ResourceTable t ON t.myId = f.myResourcePid " + + "WHERE f.myResourceType = :resource_type AND f.myForcedId IN ( :forced_id ) AND (f.myPartitionIdValue IS NULL OR f.myPartitionIdValue IN :partition_id)") + Collection findAndResolveByForcedIdWithNoTypeInPartitionIdOrNullPartitionId(@Param("resource_type") String theNextResourceType, @Param("forced_id") Collection theNextIds, @Param("forced_id") List thePartitionIdsWithoutDefault); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceTableDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceTableDao.java index 9346f5a3c87..05c10d2b005 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceTableDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceTableDao.java @@ -12,7 +12,6 @@ import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Map; -import java.util.Optional; /* * #%L @@ -65,12 +64,31 @@ public interface IResourceTableDao extends JpaRepository { @Query("DELETE FROM ResourceTable t WHERE t.myId = :pid") void deleteByPid(@Param("pid") Long theId); + /** + * This method returns a Collection where each row is an element in the collection. Each element in the collection + * is an object array, where the order matters (the array represents columns returned by the query). Be careful if you change this query in any way. + */ @Query("SELECT t.myResourceType, t.myId, t.myDeleted FROM ResourceTable t WHERE t.myId IN (:pid)") Collection findLookupFieldsByResourcePid(@Param("pid") List thePids); - @Query("SELECT t.myResourceType, t.myId, t.myDeleted FROM ResourceTable t WHERE t.myId IN (:pid) AND t.myPartitionIdValue = :partition_id") - Collection findLookupFieldsByResourcePidInPartition(@Param("pid") List thePids, @Param("partition_id") Integer thePartitionId); + /** + * This method returns a Collection where each row is an element in the collection. Each element in the collection + * is an object array, where the order matters (the array represents columns returned by the query). Be careful if you change this query in any way. + */ + @Query("SELECT t.myResourceType, t.myId, t.myDeleted FROM ResourceTable t WHERE t.myId IN (:pid) AND t.myPartitionIdValue IN :partition_id") + Collection findLookupFieldsByResourcePidInPartitionIds(@Param("pid") List thePids, @Param("partition_id") Collection thePartitionId); + /** + * This method returns a Collection where each row is an element in the collection. Each element in the collection + * is an object array, where the order matters (the array represents columns returned by the query). Be careful if you change this query in any way. + */ + @Query("SELECT t.myResourceType, t.myId, t.myDeleted FROM ResourceTable t WHERE t.myId IN (:pid) AND (t.myPartitionIdValue IS NULL OR t.myPartitionIdValue IN :partition_id)") + Collection findLookupFieldsByResourcePidInPartitionIdsOrNullPartition(@Param("pid") List thePids, @Param("partition_id") Collection thePartitionId); + + /** + * This method returns a Collection where each row is an element in the collection. Each element in the collection + * is an object array, where the order matters (the array represents columns returned by the query). Be careful if you change this query in any way. + */ @Query("SELECT t.myResourceType, t.myId, t.myDeleted FROM ResourceTable t WHERE t.myId IN (:pid) AND t.myPartitionIdValue IS NULL") Collection findLookupFieldsByResourcePidInPartitionNull(@Param("pid") List thePids); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/empi/EmpiLinkDeleteSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/empi/EmpiLinkDeleteSvc.java index 4a723b15e6d..5ea233500a6 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/empi/EmpiLinkDeleteSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/empi/EmpiLinkDeleteSvc.java @@ -40,11 +40,10 @@ public class EmpiLinkDeleteSvc { /** * Delete all EmpiLink records with any reference to this resource. (Used by Expunge.) - * @param theResource * @return the number of records deleted */ public int deleteWithAnyReferenceTo(IBaseResource theResource) { - Long pid = myIdHelperService.getPidOrThrowException(theResource.getIdElement(), null); + Long pid = myIdHelperService.getPidOrThrowException(theResource.getIdElement()); int removed = myEmpiLinkDao.deleteWithAnyReferenceToPid(pid); if (removed > 0) { ourLog.info("Removed {} EMPI links with references to {}", removed, theResource.getIdElement().toVersionless()); @@ -53,7 +52,7 @@ public class EmpiLinkDeleteSvc { } public int deleteNonRedirectWithWithAnyReferenceTo(IBaseResource theResource) { - Long pid = myIdHelperService.getPidOrThrowException(theResource.getIdElement(), null); + Long pid = myIdHelperService.getPidOrThrowException(theResource.getIdElement()); int removed = myEmpiLinkDao.deleteWithAnyReferenceToPidAndMatchResultNot(pid, EmpiMatchResultEnum.REDIRECT); if (removed > 0) { ourLog.info("Removed {} non-redirect EMPI links with references to {}", removed, theResource.getIdElement().toVersionless()); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DaoResourceLinkResolver.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DaoResourceLinkResolver.java index 8acfddbf04b..9fa15dc0384 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DaoResourceLinkResolver.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DaoResourceLinkResolver.java @@ -68,7 +68,7 @@ public class DaoResourceLinkResolver implements IResourceLinkResolver { IResourceLookup resolvedResource; String idPart = theSourceResourceId.getIdPart(); try { - resolvedResource = myIdHelperService.resolveResourceIdentity(theRequestPartitionId, theResourceType, idPart, theRequest); + resolvedResource = myIdHelperService.resolveResourceIdentity(theRequestPartitionId, theResourceType, idPart); ourLog.trace("Translated {}/{} to resource PID {}", theType, idPart, resolvedResource); } catch (ResourceNotFoundException e) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java index c51453522b1..e759e97a02b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java @@ -21,7 +21,6 @@ package ca.uhn.fhir.jpa.dao.index; */ import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.dao.data.IForcedIdDao; @@ -33,7 +32,6 @@ import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.util.MemoryCacheService; import ca.uhn.fhir.jpa.util.QueryChunker; import ca.uhn.fhir.model.primitive.IdDt; -import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId; import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; @@ -44,10 +42,7 @@ import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.stereotype.Service; import javax.annotation.Nonnull; @@ -62,7 +57,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.function.Function; import java.util.stream.Collectors; import static org.apache.commons.lang3.StringUtils.isBlank; @@ -87,7 +81,6 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; */ @Service public class IdHelperService { - private static final Logger ourLog = LoggerFactory.getLogger(IdHelperService.class); private static final String RESOURCE_PID = "RESOURCE_PID"; @Autowired @@ -97,8 +90,6 @@ public class IdHelperService { @Autowired private DaoConfig myDaoConfig; @Autowired - private IInterceptorBroadcaster myInterceptorBroadcaster; - @Autowired private FhirContext myFhirCtx; @Autowired private MemoryCacheService myMemoryCacheService; @@ -114,14 +105,25 @@ public class IdHelperService { * @throws ResourceNotFoundException If the ID can not be found */ @Nonnull - public IResourceLookup resolveResourceIdentity(@Nonnull RequestPartitionId theRequestPartitionId, String theResourceType, String theResourceId, RequestDetails theRequestDetails) throws ResourceNotFoundException { + public IResourceLookup resolveResourceIdentity(@Nonnull RequestPartitionId theRequestPartitionId, String theResourceType, String theResourceId) throws ResourceNotFoundException { // We only pass 1 input in so only 0..1 will come back IdDt id = new IdDt(theResourceType, theResourceId); - Collection matches = translateForcedIdToPids(theRequestPartitionId, theRequestDetails, Collections.singletonList(id)); - assert matches.size() <= 1; + Collection matches = translateForcedIdToPids(theRequestPartitionId, Collections.singletonList(id)); + if (matches.isEmpty()) { throw new ResourceNotFoundException(id); } + + if (matches.size() > 1) { + /* + * This means that: + * 1. There are two resources with the exact same resource type and forced id + * 2. The unique constraint on this column-pair has been dropped + */ + String msg = myFhirCtx.getLocalizer().getMessage(IdHelperService.class, "nonUniqueForcedId"); + throw new PreconditionFailedException(msg); + } + return matches.iterator().next(); } @@ -137,10 +139,10 @@ public class IdHelperService { Long retVal; if (myDaoConfig.getResourceClientIdStrategy() == DaoConfig.ClientIdStrategyEnum.ANY || !isValidPid(theId)) { if (myDaoConfig.isDeleteEnabled()) { - retVal = resolveResourceIdentity(theRequestPartitionId, theResourceType, theId); + retVal = resolveResourceIdentity(theRequestPartitionId, theResourceType, theId).getResourceId(); } else { String key = RequestPartitionId.stringifyForKey(theRequestPartitionId) + "/" + theResourceType + "/" + theId; - retVal = myMemoryCacheService.get(MemoryCacheService.CacheEnum.PERSISTENT_ID, key, t -> resolveResourceIdentity(theRequestPartitionId, theResourceType, theId)); + retVal = myMemoryCacheService.get(MemoryCacheService.CacheEnum.PERSISTENT_ID, key, t -> resolveResourceIdentity(theRequestPartitionId, theResourceType, theId).getResourceId()); } } else { @@ -187,9 +189,10 @@ public class IdHelperService { } else { + String partitionIdStringForKey = RequestPartitionId.stringifyForKey(theRequestPartitionId); for (Iterator idIterator = nextIds.iterator(); idIterator.hasNext(); ) { String nextId = idIterator.next(); - String key = RequestPartitionId.stringifyForKey(theRequestPartitionId) + "/" + nextResourceType + "/" + nextId; + String key = partitionIdStringForKey + "/" + nextResourceType + "/" + nextId; Long nextCachedPid = myMemoryCacheService.getIfPresent(MemoryCacheService.CacheEnum.PERSISTENT_ID, key); if (nextCachedPid != null) { idIterator.remove(); @@ -203,10 +206,12 @@ public class IdHelperService { if (theRequestPartitionId.isAllPartitions()) { views = myForcedIdDao.findByTypeAndForcedId(nextResourceType, nextIds); } else { - if (theRequestPartitionId.getPartitionId() != null) { - views = myForcedIdDao.findByTypeAndForcedIdInPartition(nextResourceType, nextIds, theRequestPartitionId.getPartitionId()); - } else { + if (theRequestPartitionId.isDefaultPartition()) { views = myForcedIdDao.findByTypeAndForcedIdInPartitionNull(nextResourceType, nextIds); + } else if (theRequestPartitionId.hasDefaultPartitionId()) { + views = myForcedIdDao.findByTypeAndForcedIdInPartitionIdsOrNullPartition(nextResourceType, nextIds, theRequestPartitionId.getPartitionIds()); + } else { + views = myForcedIdDao.findByTypeAndForcedIdInPartitionIds(nextResourceType, nextIds, theRequestPartitionId.getPartitionIds()); } } for (Object[] nextView : views) { @@ -214,7 +219,7 @@ public class IdHelperService { Long pid = (Long) nextView[1]; retVal.add(new ResourcePersistentId(pid)); - String key = RequestPartitionId.stringifyForKey(theRequestPartitionId) + "/" + nextResourceType + "/" + forcedId; + String key = partitionIdStringForKey + "/" + nextResourceType + "/" + forcedId; myMemoryCacheService.put(MemoryCacheService.CacheEnum.PERSISTENT_ID, key, pid); } } @@ -261,35 +266,7 @@ public class IdHelperService { return typeToIds; } - private Long resolveResourceIdentity(@Nonnull RequestPartitionId theRequestPartitionId, @Nonnull String theResourceType, @Nonnull String theId) { - Optional pid; - if (theRequestPartitionId.isAllPartitions()) { - try { - pid = myForcedIdDao.findByTypeAndForcedId(theResourceType, theId); - } catch (IncorrectResultSizeDataAccessException e) { - /* - * This means that: - * 1. There are two resources with the exact same resource type and forced id - * 2. The unique constraint on this column-pair has been dropped - */ - String msg = myFhirCtx.getLocalizer().getMessage(IdHelperService.class, "nonUniqueForcedId"); - throw new PreconditionFailedException(msg); - } - } else { - if (theRequestPartitionId.getPartitionId() == null) { - pid = myForcedIdDao.findByPartitionIdNullAndTypeAndForcedId(theResourceType, theId); - } else { - pid = myForcedIdDao.findByPartitionIdAndTypeAndForcedId(theRequestPartitionId.getPartitionId(), theResourceType, theId); - } - } - - if (!pid.isPresent()) { - throw new ResourceNotFoundException(new IdDt(theResourceType, theId)); - } - return pid.get(); - } - - private Collection translateForcedIdToPids(@Nonnull RequestPartitionId theRequestPartitionId, RequestDetails theRequest, Collection theId) { + private Collection translateForcedIdToPids(@Nonnull RequestPartitionId theRequestPartitionId, Collection theId) { theId.forEach(id -> Validate.isTrue(id.hasIdPart())); if (theId.isEmpty()) { @@ -333,10 +310,12 @@ public class IdHelperService { if (theRequestPartitionId.isAllPartitions()) { views = myForcedIdDao.findAndResolveByForcedIdWithNoType(nextResourceType, nextIds); } else { - if (theRequestPartitionId.getPartitionId() != null) { - views = myForcedIdDao.findAndResolveByForcedIdWithNoTypeInPartition(nextResourceType, nextIds, theRequestPartitionId.getPartitionId()); - } else { + if (theRequestPartitionId.isDefaultPartition()) { views = myForcedIdDao.findAndResolveByForcedIdWithNoTypeInPartitionNull(nextResourceType, nextIds); + } else if (theRequestPartitionId.hasDefaultPartitionId()) { + views = myForcedIdDao.findAndResolveByForcedIdWithNoTypeInPartitionIdOrNullPartitionId(nextResourceType, nextIds, theRequestPartitionId.getPartitionIdsWithoutDefault()); + } else { + views = myForcedIdDao.findAndResolveByForcedIdWithNoTypeInPartition(nextResourceType, nextIds, theRequestPartitionId.getPartitionIds()); } } @@ -379,10 +358,12 @@ public class IdHelperService { if (theRequestPartitionId.isAllPartitions()) { lookup = myResourceTableDao.findLookupFieldsByResourcePid(thePidsToResolve); } else { - if (theRequestPartitionId.getPartitionId() != null) { - lookup = myResourceTableDao.findLookupFieldsByResourcePidInPartition(thePidsToResolve, theRequestPartitionId.getPartitionId()); - } else { + if (theRequestPartitionId.isDefaultPartition()) { lookup = myResourceTableDao.findLookupFieldsByResourcePidInPartitionNull(thePidsToResolve); + } else if (theRequestPartitionId.hasDefaultPartitionId()) { + lookup = myResourceTableDao.findLookupFieldsByResourcePidInPartitionIdsOrNullPartition(thePidsToResolve, theRequestPartitionId.getPartitionIdsWithoutDefault()); + } else { + lookup = myResourceTableDao.findLookupFieldsByResourcePidInPartitionIds(thePidsToResolve, theRequestPartitionId.getPartitionIds()); } } lookup @@ -448,23 +429,14 @@ public class IdHelperService { @Nonnull public Long getPidOrThrowException(IIdType theId) { - return getPidOrThrowException(theId, null); - } - - @Nonnull - public Long getPidOrThrowException(IAnyResource theResource) { - return (Long) theResource.getUserData(RESOURCE_PID); - } - - @Nonnull - public Long getPidOrThrowException(IIdType theId, RequestDetails theRequestDetails) { List ids = Collections.singletonList(theId); List resourcePersistentIds = this.resolveResourcePersistentIdsWithCache(RequestPartitionId.allPartitions(), ids); return resourcePersistentIds.get(0).getIdAsLong(); } - public Map getPidToIdMap(Collection theIds, RequestDetails theRequestDetails) { - return theIds.stream().collect(Collectors.toMap(this::getPidOrThrowException, Function.identity())); + @Nonnull + public Long getPidOrThrowException(IAnyResource theResource) { + return (Long) theResource.getUserData(RESOURCE_PID); } public IIdType resourceIdFromPidOrThrowException(Long thePid) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/SearchParamWithInlineReferencesExtractor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/SearchParamWithInlineReferencesExtractor.java index c7982122ec6..1b78788f013 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/SearchParamWithInlineReferencesExtractor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/SearchParamWithInlineReferencesExtractor.java @@ -25,6 +25,7 @@ import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.api.config.DaoConfig; +import ca.uhn.fhir.jpa.model.entity.PartitionablePartitionId; import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; import ca.uhn.fhir.jpa.dao.MatchResourceUrlService; @@ -99,7 +100,7 @@ public class SearchParamWithInlineReferencesExtractor { RequestPartitionId partitionId; if (myPartitionSettings.isPartitioningEnabled()) { - partitionId = theEntity.getPartitionId(); + partitionId = PartitionablePartitionId.toRequestPartitionId(theEntity.getPartitionId()); } else { partitionId = RequestPartitionId.allPartitions(); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/BasePredicateBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/BasePredicateBuilder.java index 340e009e343..cf81e4a41f9 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/BasePredicateBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/BasePredicateBuilder.java @@ -92,10 +92,10 @@ abstract class BasePredicateBuilder { void addPredicateParamMissingForNonReference(String theResourceName, String theParamName, boolean theMissing, From theJoin, RequestPartitionId theRequestPartitionId) { if (!theRequestPartitionId.isAllPartitions()) { - if (theRequestPartitionId.getPartitionId() != null) { - myQueryStack.addPredicate(myCriteriaBuilder.equal(theJoin.get("myPartitionIdValue"), theRequestPartitionId.getPartitionId())); - } else { + if (theRequestPartitionId.isDefaultPartition()) { myQueryStack.addPredicate(myCriteriaBuilder.isNull(theJoin.get("myPartitionIdValue"))); + } else { + myQueryStack.addPredicate(theJoin.get("myPartitionIdValue").in(theRequestPartitionId.getPartitionIds())); } } myQueryStack.addPredicateWithImplicitTypeSelection(myCriteriaBuilder.equal(theJoin.get("myResourceType"), theResourceName)); @@ -184,12 +184,11 @@ abstract class BasePredicateBuilder { void addPartitionIdPredicate(RequestPartitionId theRequestPartitionId, From theJoin, List theCodePredicates) { if (!theRequestPartitionId.isAllPartitions()) { - Integer partitionId = theRequestPartitionId.getPartitionId(); Predicate partitionPredicate; - if (partitionId != null) { - partitionPredicate = myCriteriaBuilder.equal(theJoin.get("myPartitionIdValue").as(Integer.class), partitionId); - } else { + if (theRequestPartitionId.isDefaultPartition()) { partitionPredicate = myCriteriaBuilder.isNull(theJoin.get("myPartitionIdValue").as(Integer.class)); + } else { + partitionPredicate = theJoin.get("myPartitionIdValue").as(Integer.class).in(theRequestPartitionId.getPartitionIds()); } myQueryStack.addPredicate(partitionPredicate); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderToken.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderToken.java index 718bee5349d..2c24be1d5e8 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderToken.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderToken.java @@ -151,7 +151,7 @@ class PredicateBuilderToken extends BasePredicateBuilder implements IPredicateBu theBuilder, theFrom, null, - theRequestPartitionId); + theRequestPartitionId); } private Collection createPredicateToken(Collection theParameters, diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/querystack/QueryRootEntryResourceTable.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/querystack/QueryRootEntryResourceTable.java index 3f159876751..684e8b61870 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/querystack/QueryRootEntryResourceTable.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/querystack/QueryRootEntryResourceTable.java @@ -63,8 +63,8 @@ class QueryRootEntryResourceTable extends QueryRootEntry { } addPredicate(myCriteriaBuilder.isNull(getRoot().get("myDeleted"))); if (!myRequestPartitionId.isAllPartitions()) { - if (myRequestPartitionId.getPartitionId() != null) { - addPredicate(myCriteriaBuilder.equal(getRoot().get("myPartitionIdValue").as(Integer.class), myRequestPartitionId.getPartitionId())); + if (!myRequestPartitionId.isDefaultPartition()) { + addPredicate(getRoot().get("myPartitionIdValue").as(Integer.class).in(myRequestPartitionId.getPartitionIds())); } else { addPredicate(myCriteriaBuilder.isNull(getRoot().get("myPartitionIdValue").as(Integer.class))); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceSearchView.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceSearchView.java index 733fb2868fd..b572d5e1db0 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceSearchView.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceSearchView.java @@ -24,6 +24,7 @@ import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.model.entity.ForcedId; import ca.uhn.fhir.jpa.model.entity.IBaseResourceEntity; +import ca.uhn.fhir.jpa.model.entity.PartitionablePartitionId; import ca.uhn.fhir.jpa.model.entity.ResourceEncodingEnum; import ca.uhn.fhir.jpa.model.entity.ResourceHistoryProvenanceEntity; import ca.uhn.fhir.model.primitive.IdDt; @@ -32,6 +33,7 @@ import ca.uhn.fhir.rest.api.Constants; import org.hibernate.annotations.Immutable; import org.hibernate.annotations.Subselect; +import javax.annotation.Nullable; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.EnumType; @@ -199,8 +201,13 @@ public class ResourceSearchView implements IBaseResourceEntity, Serializable { } @Override - public RequestPartitionId getPartitionId() { - return RequestPartitionId.fromPartitionId(myPartitionId); + @Nullable + public PartitionablePartitionId getPartitionId() { + if (myPartitionId != null) { + return new PartitionablePartitionId(myPartitionId, null); + } else { + return null; + } } public byte[] getResource() { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/IPartitionLookupSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/IPartitionLookupSvc.java index 95c783d9522..cb5c389c635 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/IPartitionLookupSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/IPartitionLookupSvc.java @@ -23,6 +23,8 @@ package ca.uhn.fhir.jpa.partition; import ca.uhn.fhir.jpa.entity.PartitionEntity; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import javax.annotation.Nullable; + public interface IPartitionLookupSvc { /** @@ -33,6 +35,7 @@ public interface IPartitionLookupSvc { /** * @throws ResourceNotFoundException If the name is not known */ + @Nullable PartitionEntity getPartitionByName(String theName) throws ResourceNotFoundException; /** diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/PartitionLookupSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/PartitionLookupSvcImpl.java index 862f2b4f0fe..81054fca6d9 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/PartitionLookupSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/PartitionLookupSvcImpl.java @@ -23,6 +23,7 @@ package ca.uhn.fhir.jpa.partition; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.dao.data.IPartitionDao; import ca.uhn.fhir.jpa.entity.PartitionEntity; +import ca.uhn.fhir.jpa.model.util.JpaConstants; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import com.github.benmanes.caffeine.cache.CacheLoader; @@ -47,9 +48,6 @@ import static org.apache.commons.lang3.StringUtils.isBlank; public class PartitionLookupSvcImpl implements IPartitionLookupSvc { - public static final int DEFAULT_PERSISTED_PARTITION_ID = 0; - public static final String DEFAULT_PERSISTED_PARTITION_NAME = "DEFAULT"; - private static final String DEFAULT_PERSISTED_PARTITION_DESC = "Default partition"; private static final Pattern PARTITION_NAME_VALID_PATTERN = Pattern.compile("[a-zA-Z0-9_-]+"); private static final Logger ourLog = LoggerFactory.getLogger(PartitionLookupSvcImpl.class); @@ -76,23 +74,14 @@ public class PartitionLookupSvcImpl implements IPartitionLookupSvc { .expireAfterWrite(1, TimeUnit.MINUTES) .build(new IdToPartitionCacheLoader()); myTxTemplate = new TransactionTemplate(myTxManager); - - // Create default partition definition if it doesn't already exist - myTxTemplate.executeWithoutResult(t -> { - if (myPartitionDao.findById(DEFAULT_PERSISTED_PARTITION_ID).isPresent() == false) { - ourLog.info("Creating default partition definition"); - PartitionEntity partitionEntity = new PartitionEntity(); - partitionEntity.setId(DEFAULT_PERSISTED_PARTITION_ID); - partitionEntity.setName(DEFAULT_PERSISTED_PARTITION_NAME); - partitionEntity.setDescription(DEFAULT_PERSISTED_PARTITION_DESC); - myPartitionDao.save(partitionEntity); - } - }); } @Override public PartitionEntity getPartitionByName(String theName) { Validate.notBlank(theName, "The name must not be null or blank"); + if (JpaConstants.DEFAULT_PARTITION_NAME.equals(theName)) { + return null; + } return myNameToPartitionCache.get(theName); } @@ -114,11 +103,6 @@ public class PartitionLookupSvcImpl implements IPartitionLookupSvc { validateHaveValidPartitionIdAndName(thePartition); validatePartitionNameDoesntAlreadyExist(thePartition.getName()); - if (thePartition.getId() == DEFAULT_PERSISTED_PARTITION_ID) { - String msg = myFhirCtx.getLocalizer().getMessage(PartitionLookupSvcImpl.class, "cantCreatePartition0"); - throw new InvalidRequestException(msg); - } - ourLog.info("Creating new partition with ID {} and Name {}", thePartition.getId(), thePartition.getName()); myPartitionDao.save(thePartition); @@ -141,13 +125,6 @@ public class PartitionLookupSvcImpl implements IPartitionLookupSvc { validatePartitionNameDoesntAlreadyExist(thePartition.getName()); } - if (DEFAULT_PERSISTED_PARTITION_ID == thePartition.getId()) { - if (!DEFAULT_PERSISTED_PARTITION_NAME.equals(thePartition.getName())) { - String msg = myFhirCtx.getLocalizer().getMessageSanitized(PartitionLookupSvcImpl.class, "cantRenameDefaultPartition"); - throw new InvalidRequestException(msg); - } - } - existingPartition.setName(thePartition.getName()); existingPartition.setDescription(thePartition.getDescription()); myPartitionDao.save(existingPartition); @@ -160,11 +137,6 @@ public class PartitionLookupSvcImpl implements IPartitionLookupSvc { public void deletePartition(Integer thePartitionId) { validatePartitionIdSupplied(myFhirCtx, thePartitionId); - if (DEFAULT_PERSISTED_PARTITION_ID == thePartitionId) { - String msg = myFhirCtx.getLocalizer().getMessageSanitized(PartitionLookupSvcImpl.class, "cantDeleteDefaultPartition"); - throw new InvalidRequestException(msg); - } - Optional partition = myPartitionDao.findById(thePartitionId); if (!partition.isPresent()) { String msg = myFhirCtx.getLocalizer().getMessageSanitized(PartitionLookupSvcImpl.class, "unknownPartitionId", thePartitionId); @@ -189,6 +161,11 @@ public class PartitionLookupSvcImpl implements IPartitionLookupSvc { throw new InvalidRequestException(msg); } + if (thePartition.getName().equals(JpaConstants.DEFAULT_PARTITION_NAME)) { + String msg = myFhirCtx.getLocalizer().getMessageSanitized(PartitionLookupSvcImpl.class, "cantCreateDefaultPartition"); + throw new InvalidRequestException(msg); + } + if (!PARTITION_NAME_VALID_PATTERN.matcher(thePartition.getName()).matches()) { String msg = myFhirCtx.getLocalizer().getMessageSanitized(PartitionLookupSvcImpl.class, "invalidName", thePartition.getName()); throw new InvalidRequestException(msg); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java index 9380296134b..ae65806e9d5 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java @@ -27,9 +27,9 @@ import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.entity.PartitionEntity; import ca.uhn.fhir.jpa.model.config.PartitionSettings; +import ca.uhn.fhir.jpa.model.util.JpaConstants; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; @@ -39,7 +39,10 @@ import org.springframework.beans.factory.annotation.Autowired; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; +import java.util.Objects; import static ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster.doCallHooks; import static ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster.doCallHooksAndReturnObject; @@ -102,9 +105,9 @@ public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc { requestPartitionId = null; } - validatePartition(requestPartitionId, theResourceType, Pointcut.STORAGE_PARTITION_IDENTIFY_READ); + validateRequestPartitionNotNull(requestPartitionId, Pointcut.STORAGE_PARTITION_IDENTIFY_READ); - return normalizeAndNotifyHooks(requestPartitionId, theRequest); + return validateNormalizeAndNotifyHooksForRead(requestPartitionId, theRequest); } return RequestPartitionId.allPartitions(); @@ -132,48 +135,29 @@ public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc { requestPartitionId = (RequestPartitionId) doCallHooksAndReturnObject(myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE, params); String resourceName = myFhirContext.getResourceType(theResource); - validatePartition(requestPartitionId, resourceName, Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE); + validateSinglePartitionForCreate(requestPartitionId, resourceName, Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE); - return normalizeAndNotifyHooks(requestPartitionId, theRequest); + return validateNormalizeAndNotifyHooksForRead(requestPartitionId, theRequest); } return RequestPartitionId.allPartitions(); } /** - * If the partition only has a name but not an ID, this method resolves the ID + * If the partition only has a name but not an ID, this method resolves the ID. + *

+ * If the partition has an ID but not a name, the name is resolved. + *

+ * If the partition has both, they are validated to ensure that they correspond. */ @Nonnull - private RequestPartitionId normalizeAndNotifyHooks(@Nonnull RequestPartitionId theRequestPartitionId, RequestDetails theRequest) { + private RequestPartitionId validateNormalizeAndNotifyHooksForRead(@Nonnull RequestPartitionId theRequestPartitionId, RequestDetails theRequest) { RequestPartitionId retVal = theRequestPartitionId; - if (retVal.getPartitionName() != null) { - - PartitionEntity partition; - try { - partition = myPartitionConfigSvc.getPartitionByName(retVal.getPartitionName()); - } catch (IllegalArgumentException e) { - String msg = myFhirContext.getLocalizer().getMessage(RequestPartitionHelperSvc.class, "unknownPartitionName", retVal.getPartitionName()); - throw new ResourceNotFoundException(msg); - } - - if (retVal.getPartitionId() != null) { - Validate.isTrue(retVal.getPartitionId().equals(partition.getId()), "Partition name %s does not match ID %n", retVal.getPartitionName(), retVal.getPartitionId()); - } else { - retVal = RequestPartitionId.forPartitionIdAndName(partition.getId(), retVal.getPartitionName(), retVal.getPartitionDate()); - } - - } else if (retVal.getPartitionId() != null) { - - PartitionEntity partition; - try { - partition = myPartitionConfigSvc.getPartitionById(retVal.getPartitionId()); - } catch (IllegalArgumentException e) { - String msg = myFhirContext.getLocalizer().getMessage(RequestPartitionHelperSvc.class, "unknownPartitionId", retVal.getPartitionId()); - throw new ResourceNotFoundException(msg); - } - retVal = RequestPartitionId.forPartitionIdAndName(partition.getId(), partition.getName(), retVal.getPartitionDate()); - + if (retVal.getPartitionNames() != null) { + retVal = validateAndNormalizePartitionNames(retVal); + } else if (retVal.hasPartitionIds()) { + retVal = validateAndNormalizePartitionIds(retVal); } // Note: It's still possible that the partition only has a date but no name/id @@ -188,27 +172,117 @@ public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc { } - private void validatePartition(RequestPartitionId theRequestPartitionId, @Nonnull String theResourceName, Pointcut thePointcut) { - if (theRequestPartitionId == null) { - throw new InternalErrorException("No interceptor provided a value for pointcut: " + thePointcut); + private RequestPartitionId validateAndNormalizePartitionIds(RequestPartitionId theRequestPartitionId) { + List names = null; + for (int i = 0; i < theRequestPartitionId.getPartitionIds().size(); i++) { + + PartitionEntity partition; + Integer id = theRequestPartitionId.getPartitionIds().get(i); + if (id == null) { + partition = null; + } else { + try { + partition = myPartitionConfigSvc.getPartitionById(id); + } catch (IllegalArgumentException e) { + String msg = myFhirContext.getLocalizer().getMessage(RequestPartitionHelperSvc.class, "unknownPartitionId", theRequestPartitionId.getPartitionIds().get(i)); + throw new ResourceNotFoundException(msg); + } + } + + if (theRequestPartitionId.getPartitionNames() != null) { + if (partition == null) { + Validate.isTrue(theRequestPartitionId.getPartitionIds().get(i) == null, "Partition %s must not have an ID", JpaConstants.DEFAULT_PARTITION_NAME); + } else { + Validate.isTrue(Objects.equals(theRequestPartitionId.getPartitionIds().get(i), partition.getId()), "Partition name %s does not match ID %n", theRequestPartitionId.getPartitionNames().get(i), theRequestPartitionId.getPartitionIds().get(i)); + } + } else { + if (names == null) { + names = new ArrayList<>(); + } + if (partition != null) { + names.add(partition.getName()); + } else { + names.add(null); + } + } + } - if (theRequestPartitionId.getPartitionId() != null) { + if (names != null) { + return RequestPartitionId.forPartitionIdsAndNames(names, theRequestPartitionId.getPartitionIds(), theRequestPartitionId.getPartitionDate()); + } + + return theRequestPartitionId; + } + + private RequestPartitionId validateAndNormalizePartitionNames(RequestPartitionId theRequestPartitionId) { + List ids = null; + for (int i = 0; i < theRequestPartitionId.getPartitionNames().size(); i++) { + + PartitionEntity partition; + try { + partition = myPartitionConfigSvc.getPartitionByName(theRequestPartitionId.getPartitionNames().get(i)); + } catch (IllegalArgumentException e) { + String msg = myFhirContext.getLocalizer().getMessage(RequestPartitionHelperSvc.class, "unknownPartitionName", theRequestPartitionId.getPartitionNames().get(i)); + throw new ResourceNotFoundException(msg); + } + + if (theRequestPartitionId.hasPartitionIds()) { + if (partition == null) { + Validate.isTrue(theRequestPartitionId.getPartitionIds().get(i) == null, "Partition %s must not have an ID", JpaConstants.DEFAULT_PARTITION_NAME); + } else { + Validate.isTrue(Objects.equals(theRequestPartitionId.getPartitionIds().get(i), partition.getId()), "Partition name %s does not match ID %n", theRequestPartitionId.getPartitionNames().get(i), theRequestPartitionId.getPartitionIds().get(i)); + } + } else { + if (ids == null) { + ids = new ArrayList<>(); + } + if (partition != null) { + ids.add(partition.getId()); + } else { + ids.add(null); + } + } + + } + + if (ids != null) { + return RequestPartitionId.forPartitionIdsAndNames(theRequestPartitionId.getPartitionNames(), ids, theRequestPartitionId.getPartitionDate()); + } + + return theRequestPartitionId; + } + + private void validateSinglePartitionForCreate(RequestPartitionId theRequestPartitionId, @Nonnull String theResourceName, Pointcut thePointcut) { + validateRequestPartitionNotNull(theRequestPartitionId, thePointcut); + + if (theRequestPartitionId.hasPartitionIds()) { + validateSinglePartitionIdOrNameForCreate(theRequestPartitionId.getPartitionIds()); + } + validateSinglePartitionIdOrNameForCreate(theRequestPartitionId.getPartitionNames()); + + // Make sure we're not using one of the conformance resources in a non-default partition + if ((theRequestPartitionId.hasPartitionIds() && !theRequestPartitionId.getPartitionIds().contains(null)) || + (theRequestPartitionId.hasPartitionNames() && !theRequestPartitionId.getPartitionNames().contains(JpaConstants.DEFAULT_PARTITION_NAME))) { - // Make sure we're not using one of the conformance resources in a non-default partition if (myPartitioningBlacklist.contains(theResourceName)) { String msg = myFhirContext.getLocalizer().getMessageSanitized(RequestPartitionHelperSvc.class, "blacklistedResourceTypeForPartitioning", theResourceName); throw new UnprocessableEntityException(msg); } - // Make sure the partition exists - try { - myPartitionConfigSvc.getPartitionById(theRequestPartitionId.getPartitionId()); - } catch (IllegalArgumentException e) { - String msg = myFhirContext.getLocalizer().getMessageSanitized(RequestPartitionHelperSvc.class, "unknownPartitionId", theRequestPartitionId.getPartitionId()); - throw new InvalidRequestException(msg); - } + } + } + + private void validateRequestPartitionNotNull(RequestPartitionId theTheRequestPartitionId, Pointcut theThePointcut) { + if (theTheRequestPartitionId == null) { + throw new InternalErrorException("No interceptor provided a value for pointcut: " + theThePointcut); + } + } + + private void validateSinglePartitionIdOrNameForCreate(@Nullable List thePartitionIds) { + if (thePartitionIds != null && thePartitionIds.size() != 1) { + throw new InternalErrorException("RequestPartitionId must contain a single partition for create operations, found: " + thePartitionIds); } } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/BaseJoiningPredicateBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/BaseJoiningPredicateBuilder.java index 056fcc0f874..3f69ba4fccb 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/BaseJoiningPredicateBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/BaseJoiningPredicateBuilder.java @@ -24,6 +24,7 @@ import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder; import com.healthmarketscience.sqlbuilder.BinaryCondition; import com.healthmarketscience.sqlbuilder.Condition; +import com.healthmarketscience.sqlbuilder.InCondition; import com.healthmarketscience.sqlbuilder.NotCondition; import com.healthmarketscience.sqlbuilder.UnaryCondition; import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn; @@ -35,6 +36,7 @@ import java.util.List; import static ca.uhn.fhir.jpa.search.builder.QueryStack.toAndPredicate; import static ca.uhn.fhir.jpa.search.builder.QueryStack.toEqualToOrInPredicate; +import static ca.uhn.fhir.jpa.search.builder.QueryStack.toOrPredicate; public abstract class BaseJoiningPredicateBuilder extends BasePredicateBuilder { @@ -70,12 +72,16 @@ public abstract class BaseJoiningPredicateBuilder extends BasePredicateBuilder { public Condition createPartitionIdPredicate(RequestPartitionId theRequestPartitionId) { if (theRequestPartitionId != null && !theRequestPartitionId.isAllPartitions()) { Condition condition; - Integer partitionId = theRequestPartitionId.getPartitionId(); - if (partitionId != null) { - Object placeholder = generatePlaceholder(partitionId); - condition = BinaryCondition.equalTo(getPartitionIdColumn(), placeholder); - } else { + if (theRequestPartitionId.isDefaultPartition()) { condition = UnaryCondition.isNull(getPartitionIdColumn()); + } else if (theRequestPartitionId.hasDefaultPartitionId()) { + List placeholders = generatePlaceholders(theRequestPartitionId.getPartitionIdsWithoutDefault()); + UnaryCondition partitionNullPredicate = UnaryCondition.isNull(getPartitionIdColumn()); + InCondition partitionIdsPredicate = new InCondition(getPartitionIdColumn(), placeholders); + condition = toOrPredicate(partitionNullPredicate, partitionIdsPredicate); + } else { + List placeholders = generatePlaceholders(theRequestPartitionId.getPartitionIds()); + condition = new InCondition(getPartitionIdColumn(), placeholders); } return condition; } else { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java index 333fc1c5b72..68ba2a59431 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java @@ -22,6 +22,7 @@ import ca.uhn.fhir.jpa.config.TestR4Config; import ca.uhn.fhir.jpa.dao.BaseJpaTest; import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc; import ca.uhn.fhir.jpa.dao.data.IForcedIdDao; +import ca.uhn.fhir.jpa.dao.data.IPartitionDao; import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTableDao; import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTagDao; import ca.uhn.fhir.jpa.dao.data.IResourceIndexedCompositeStringUniqueDao; @@ -185,6 +186,8 @@ public abstract class BaseJpaR4Test extends BaseJpaTest implements ITestDataBuil @Autowired protected IPartitionLookupSvc myPartitionConfigSvc; @Autowired + protected IPartitionDao myPartitionDao; + @Autowired protected ITermReadSvc myHapiTerminologySvc; @Autowired protected CachingValidationSupport myCachingValidationSupport; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningSqlR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningSqlR4Test.java index 8ebfa6ee2c4..731b91a5279 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningSqlR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningSqlR4Test.java @@ -10,6 +10,7 @@ import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.entity.PartitionEntity; import ca.uhn.fhir.jpa.model.config.PartitionSettings; import ca.uhn.fhir.jpa.model.entity.ForcedId; +import ca.uhn.fhir.jpa.model.entity.PartitionablePartitionId; import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable; import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTag; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedCompositeStringUnique; @@ -19,6 +20,7 @@ import ca.uhn.fhir.jpa.model.entity.ResourceLink; import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.model.entity.ResourceTag; import ca.uhn.fhir.jpa.model.entity.SearchParamPresent; +import ca.uhn.fhir.jpa.model.util.JpaConstants; import ca.uhn.fhir.jpa.partition.IPartitionLookupSvc; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.util.SqlQuery; @@ -35,6 +37,7 @@ import ca.uhn.fhir.rest.param.ReferenceParam; import ca.uhn.fhir.rest.param.StringParam; import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.rest.param.TokenParamModifier; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; @@ -74,6 +77,7 @@ import java.util.stream.Collectors; import static ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast; import static org.apache.commons.lang3.StringUtils.countMatches; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.matchesPattern; import static org.hamcrest.Matchers.startsWith; @@ -90,8 +94,11 @@ import static org.mockito.Mockito.when; @SuppressWarnings("unchecked") public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { + static final String PARTITION_1 = "PART-1"; + static final String PARTITION_2 = "PART-2"; + static final String PARTITION_3 = "PART-3"; + static final String PARTITION_4 = "PART-4"; private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(PartitioningSqlR4Test.class); - private MyReadWriteInterceptor myPartitionInterceptor; private LocalDate myPartitionDate; private LocalDate myPartitionDate2; @@ -142,11 +149,17 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { myPartitionInterceptor = new MyReadWriteInterceptor(); myInterceptorRegistry.registerInterceptor(myPartitionInterceptor); - myPartitionConfigSvc.createPartition(new PartitionEntity().setId(1).setName("PART-1")); - myPartitionConfigSvc.createPartition(new PartitionEntity().setId(2).setName("PART-2")); - myPartitionConfigSvc.createPartition(new PartitionEntity().setId(3).setName("PART-3")); + myPartitionConfigSvc.createPartition(new PartitionEntity().setId(1).setName(PARTITION_1)); + myPartitionConfigSvc.createPartition(new PartitionEntity().setId(2).setName(PARTITION_2)); + myPartitionConfigSvc.createPartition(new PartitionEntity().setId(3).setName(PARTITION_3)); + myPartitionConfigSvc.createPartition(new PartitionEntity().setId(4).setName(PARTITION_4)); myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.ENABLED); + + // Ensure the partition names are resolved + myPartitionInterceptor.addReadPartition(RequestPartitionId.fromPartitionNames(JpaConstants.DEFAULT_PARTITION_NAME, PARTITION_1, PARTITION_2, PARTITION_3, PARTITION_4)); + myPatientDao.search(new SearchParameterMap().setLoadSynchronous(true)); + } @Test @@ -162,7 +175,7 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { runInTransaction(() -> { ResourceTable resourceTable = myResourceTableDao.findById(id).orElseThrow(IllegalArgumentException::new); - assertEquals(RequestPartitionId.defaultPartition(), resourceTable.getPartitionId()); + assertEquals(null, resourceTable.getPartitionId()); }); } @@ -341,8 +354,10 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { runInTransaction(() -> { // HFJ_RESOURCE ResourceTable resourceTable = myResourceTableDao.findById(id).orElseThrow(IllegalArgumentException::new); - assertNull(resourceTable.getPartitionId().getPartitionId()); - assertEquals(myPartitionDate, resourceTable.getPartitionId().getPartitionDate()); + PartitionablePartitionId partitionId = resourceTable.getPartitionId(); + assertNotNull(partitionId); + assertNull(partitionId.getPartitionId()); + assertEquals(myPartitionDate, partitionId.getPartitionDate()); }); } @@ -393,7 +408,7 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { runInTransaction(() -> { ResourceTable resourceTable = myResourceTableDao.findById(patientId).orElseThrow(IllegalArgumentException::new); - assertEquals(RequestPartitionId.defaultPartition(), resourceTable.getPartitionId()); + assertEquals(null, resourceTable.getPartitionId()); }); } @@ -615,8 +630,8 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { // HFJ_FORCED_ID List forcedIds = myForcedIdDao.findAll(); assertEquals(2, forcedIds.size()); - assertEquals(null, forcedIds.get(0).getPartitionId().getPartitionId()); - assertEquals(null, forcedIds.get(1).getPartitionId().getPartitionId()); + assertEquals(null, forcedIds.get(0).getPartitionId()); + assertEquals(null, forcedIds.get(1).getPartitionId()); }); } @@ -883,6 +898,114 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { } } + @Test + public void testRead_PidId_MultiplePartitionNames() { + IIdType patientIdNull = createPatient(withPartition(null), withActiveTrue()); + IIdType patientId1 = createPatient(withPartition(1), withActiveTrue()); + createPatient(withPartition(2), withActiveTrue()); + IIdType patientId3 = createPatient(withPartition(3), withActiveTrue()); + + // Two partitions - Found + { + myCaptureQueriesListener.clear(); + myPartitionInterceptor.addReadPartition(RequestPartitionId.fromPartitionNames(PARTITION_1, PARTITION_2)); + IdType gotId1 = myPatientDao.read(patientId1, mySrd).getIdElement().toUnqualifiedVersionless(); + assertEquals(patientId1, gotId1); + + // Only the read columns should be used, but no selectors on partition ID + String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); + assertEquals(2, StringUtils.countMatches(searchSql, "PARTITION_ID as "), searchSql); + assertEquals(2, StringUtils.countMatches(searchSql, "PARTITION_ID"), searchSql); + } + + // Two partitions including default - Found + { + myCaptureQueriesListener.clear(); + myPartitionInterceptor.addReadPartition(RequestPartitionId.fromPartitionNames(PARTITION_1, JpaConstants.DEFAULT_PARTITION_NAME)); + IdType gotId1 = myPatientDao.read(patientIdNull, mySrd).getIdElement().toUnqualifiedVersionless(); + assertEquals(patientIdNull, gotId1); + + // Only the read columns should be used, but no selectors on partition ID + String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); + assertEquals(2, StringUtils.countMatches(searchSql, "PARTITION_ID as "), searchSql); + assertEquals(2, StringUtils.countMatches(searchSql, "PARTITION_ID"), searchSql); + } + + // Two partitions - Not Found + { + myPartitionInterceptor.addReadPartition(RequestPartitionId.fromPartitionNames(PARTITION_1, PARTITION_2)); + try { + myPatientDao.read(patientId3, mySrd); + fail(); + } catch (ResourceNotFoundException e) { + // good + } + + myPartitionInterceptor.addReadPartition(RequestPartitionId.fromPartitionNames(PARTITION_1, PARTITION_2)); + try { + myPatientDao.read(patientIdNull, mySrd); + fail(); + } catch (ResourceNotFoundException e) { + // good + } + } + + } + + @Test + public void testRead_PidId_MultiplePartitionIds() { + IIdType patientIdNull = createPatient(withPartition(null), withActiveTrue()); + IIdType patientId1 = createPatient(withPartition(1), withActiveTrue()); + createPatient(withPartition(2), withActiveTrue()); + IIdType patientId3 = createPatient(withPartition(3), withActiveTrue()); + + // Two partitions - Found + { + myCaptureQueriesListener.clear(); + myPartitionInterceptor.addReadPartition(RequestPartitionId.fromPartitionIds(1, 2)); + IdType gotId1 = myPatientDao.read(patientId1, mySrd).getIdElement().toUnqualifiedVersionless(); + assertEquals(patientId1, gotId1); + + // Only the read columns should be used, but no selectors on partition ID + String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); + assertEquals(2, StringUtils.countMatches(searchSql, "PARTITION_ID as "), searchSql); + assertEquals(2, StringUtils.countMatches(searchSql, "PARTITION_ID"), searchSql); + } + + // Two partitions including default - Found + { + myCaptureQueriesListener.clear(); + myPartitionInterceptor.addReadPartition(RequestPartitionId.fromPartitionIds(1, null)); + IdType gotId1 = myPatientDao.read(patientIdNull, mySrd).getIdElement().toUnqualifiedVersionless(); + assertEquals(patientIdNull, gotId1); + + // Only the read columns should be used, but no selectors on partition ID + String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); + assertEquals(2, StringUtils.countMatches(searchSql, "PARTITION_ID as "), searchSql); + assertEquals(2, StringUtils.countMatches(searchSql, "PARTITION_ID"), searchSql); + } + + // Two partitions - Not Found + { + myPartitionInterceptor.addReadPartition(RequestPartitionId.fromPartitionNames(PARTITION_1, PARTITION_2)); + try { + myPatientDao.read(patientId3, mySrd); + fail(); + } catch (ResourceNotFoundException e) { + // good + } + + myPartitionInterceptor.addReadPartition(RequestPartitionId.fromPartitionNames(PARTITION_1, PARTITION_2)); + try { + myPatientDao.read(patientIdNull, mySrd); + fail(); + } catch (ResourceNotFoundException e) { + // good + } + } + + } + @Test public void testRead_PidId_DefaultPartition() { IIdType patientIdNull = createPatient(withPartition(null), withActiveTrue()); @@ -1030,7 +1153,7 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { map.setLoadSynchronous(true); IBundleProvider results = myPatientDao.search(map); List ids = toUnqualifiedVersionlessIds(results); - assertThat(ids, Matchers.contains(patientIdNull, patientId1, patientId2)); + assertThat(ids, contains(patientIdNull, patientId1, patientId2)); String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); ourLog.info("Search SQL:\n{}", searchSql); @@ -1047,7 +1170,7 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { map.setLoadSynchronous(true); IBundleProvider results = myPatientDao.search(map); List ids = toUnqualifiedVersionlessIds(results); - assertThat(ids, Matchers.contains(patientIdNull, patientId1, patientId2)); + assertThat(ids, contains(patientIdNull, patientId1, patientId2)); String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); ourLog.info("Search SQL:\n{}", searchSql); @@ -1072,11 +1195,11 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { map.setLoadSynchronous(true); IBundleProvider results = myPatientDao.search(map); List ids = toUnqualifiedVersionlessIds(results); - assertThat(ids, Matchers.contains(patientId1)); + assertThat(ids, contains(patientId1)); - String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); - ourLog.info("Search SQL:\n{}", searchSql); - assertEquals(1, StringUtils.countMatches(searchSql, "t0.PARTITION_ID = '1'"), searchSql); + ourLog.info("Search SQL:\n{}", myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true)); + String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false); + assertEquals(1, StringUtils.countMatches(searchSql, "t0.PARTITION_ID IN ('1')"), searchSql); assertEquals(1, StringUtils.countMatches(searchSql, "SP_MISSING = 'true'"), searchSql); } @@ -1089,11 +1212,11 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { map.setLoadSynchronous(true); IBundleProvider results = myPatientDao.search(map); List ids = toUnqualifiedVersionlessIds(results); - assertThat(ids, Matchers.contains(patientId1)); + assertThat(ids, contains(patientId1)); - String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); - ourLog.info("Search SQL:\n{}", searchSql); - assertEquals(1, StringUtils.countMatches(searchSql, "t0.PARTITION_ID = '1'")); + ourLog.info("Search SQL:\n{}", myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true)); + String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false); + assertEquals(1, StringUtils.countMatches(searchSql, "t0.PARTITION_ID IN ('1')")); assertEquals(1, StringUtils.countMatches(searchSql, "SP_MISSING = 'false'")); } } @@ -1113,7 +1236,7 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { map.setLoadSynchronous(true); IBundleProvider results = myPatientDao.search(map); List ids = toUnqualifiedVersionlessIds(results); - assertThat(ids, Matchers.contains(patientIdNull)); + assertThat(ids, contains(patientIdNull)); String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); ourLog.info("Search SQL:\n{}", searchSql); @@ -1130,7 +1253,7 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { map.setLoadSynchronous(true); IBundleProvider results = myPatientDao.search(map); List ids = toUnqualifiedVersionlessIds(results); - assertThat(ids, Matchers.contains(patientIdNull)); + assertThat(ids, contains(patientIdNull)); String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); ourLog.info("Search SQL:\n{}", searchSql); @@ -1156,7 +1279,7 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { map.setLoadSynchronous(true); IBundleProvider results = myPatientDao.search(map); List ids = toUnqualifiedVersionlessIds(results); - assertThat(ids, Matchers.contains(patientIdNull, patientId1, patientId2)); + assertThat(ids, contains(patientIdNull, patientId1, patientId2)); String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); ourLog.info("Search SQL:\n{}", searchSql); @@ -1183,12 +1306,12 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { map.setLoadSynchronous(true); IBundleProvider results = myPatientDao.search(map); List ids = toUnqualifiedVersionlessIds(results); - assertThat(ids, Matchers.contains(patientId1)); + assertThat(ids, contains(patientId1)); - String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); - ourLog.info("Search SQL:\n{}", searchSql); + ourLog.info("Search SQL:\n{}", myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true)); + String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false); assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID"), searchSql); - assertEquals(1, StringUtils.countMatches(searchSql, "t0.PARTITION_ID = '1'"), searchSql); + assertEquals(1, StringUtils.countMatches(searchSql, "t0.PARTITION_ID IN ('1')"), searchSql); assertEquals(1, StringUtils.countMatches(searchSql, "HFJ_RES_PARAM_PRESENT"), searchSql); assertEquals(1, StringUtils.countMatches(searchSql, "HASH_PRESENCE = '-3438137196820602023'"), searchSql); } @@ -1211,12 +1334,12 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { map.setLoadSynchronous(true); IBundleProvider results = myPatientDao.search(map); List ids = toUnqualifiedVersionlessIds(results); - assertThat(ids, Matchers.contains(patientId1)); + assertThat(ids, contains(patientId1)); - String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); - ourLog.info("Search SQL:\n{}", searchSql); + ourLog.info("Search SQL:\n{}", myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true)); + String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false); assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID"), searchSql); - assertEquals(1, StringUtils.countMatches(searchSql, "t0.PARTITION_ID = '1'"), searchSql); + assertEquals(1, StringUtils.countMatches(searchSql, "t0.PARTITION_ID IN ('1')"), searchSql); assertEquals(1, StringUtils.countMatches(searchSql, "HFJ_RES_PARAM_PRESENT"), searchSql); assertEquals(1, StringUtils.countMatches(searchSql, "HASH_PRESENCE = '1919227773735728687'"), searchSql); } @@ -1237,7 +1360,7 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { map.setLoadSynchronous(true); IBundleProvider results = myPatientDao.search(map); List ids = toUnqualifiedVersionlessIds(results); - assertThat(ids, Matchers.contains(patientIdDefault)); + assertThat(ids, contains(patientIdDefault)); String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); ourLog.info("Search SQL:\n{}", searchSql); @@ -1262,7 +1385,7 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { map.setLoadSynchronous(true); IBundleProvider results = myPatientDao.search(map); List ids = toUnqualifiedVersionlessIds(results); - assertThat(ids, Matchers.contains(patientIdNull, patientId1, patientId2)); + assertThat(ids, contains(patientIdNull, patientId1, patientId2)); String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); ourLog.info("Search SQL:\n{}", searchSql); @@ -1282,13 +1405,56 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { map.setLoadSynchronous(true); IBundleProvider results = myPatientDao.search(map); List ids = toUnqualifiedVersionlessIds(results); - assertThat(ids, Matchers.contains(patientId1)); + assertThat(ids, contains(patientId1)); String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); ourLog.info("Search SQL:\n{}", searchSql); assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID")); } + @Test + public void testSearch_NoParams_SearchMultiplePartitionsByName_NoDefault() { + createPatient(withPartition(null), withActiveTrue()); + IIdType patientId1 = createPatient(withPartition(1), withActiveTrue()); + IIdType patientId2 = createPatient(withPartition(2), withActiveTrue()); + createPatient(withPartition(3), withActiveTrue()); + + addReadPartitions(PARTITION_1, PARTITION_2); + + myCaptureQueriesListener.clear(); + SearchParameterMap map = new SearchParameterMap(); + map.setLoadSynchronous(true); + IBundleProvider results = myPatientDao.search(map); + List ids = toUnqualifiedVersionlessIds(results); + assertThat(ids, Matchers.contains(patientId1, patientId2)); + + ourLog.info("Search SQL:\n{}", myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true)); + String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false); + assertThat(searchSql, containsString("PARTITION_ID IN ('1','2')")); + } + + @Test + public void testSearch_NoParams_SearchMultiplePartitionsByName_WithDefault() { + IIdType patientIdNull = createPatient(withPartition(null), withActiveTrue()); + createPatient(withPartition(1), withActiveTrue()); + IIdType patientId2 = createPatient(withPartition(2), withActiveTrue()); + createPatient(withPartition(3), withActiveTrue()); + + addReadPartitions(JpaConstants.DEFAULT_PARTITION_NAME, PARTITION_2); + + myCaptureQueriesListener.clear(); + SearchParameterMap map = new SearchParameterMap(); + map.setLoadSynchronous(true); + IBundleProvider results = myPatientDao.search(map); + List ids = toUnqualifiedVersionlessIds(results); + assertThat(ids.toString(), ids, Matchers.containsInAnyOrder(patientIdNull, patientId2)); + + ourLog.info("Search SQL:\n{}", myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true)); + String sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false); + assertThat(sql, sql, containsString("PARTITION_ID IN ('2')")); + assertThat(sql, sql, containsString("PARTITION_ID IS NULL")); + } + @Test public void testSearch_DateParam_SearchAllPartitions() { myPartitionSettings.setIncludePartitionInSearchHashes(false); @@ -1309,7 +1475,7 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { map.setLoadSynchronous(true); IBundleProvider results = myPatientDao.search(map); List ids = toUnqualifiedVersionlessIds(results); - assertThat(ids, Matchers.contains(patientIdNull, patientId1, patientId2)); + assertThat(ids, contains(patientIdNull, patientId1, patientId2)); String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); ourLog.info("Search SQL:\n{}", searchSql); @@ -1325,7 +1491,7 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { map.setLoadSynchronous(true); results = myPatientDao.search(map); ids = toUnqualifiedVersionlessIds(results); - assertThat(ids, Matchers.contains(patientIdNull, patientId1, patientId2)); + assertThat(ids, contains(patientIdNull, patientId1, patientId2)); searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); ourLog.info("Search SQL:\n{}", searchSql); @@ -1341,7 +1507,7 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { map.setLoadSynchronous(true); results = myPatientDao.search(map); ids = toUnqualifiedVersionlessIds(results); - assertThat(ids, Matchers.contains(patientIdNull, patientId1, patientId2)); + assertThat(ids, contains(patientIdNull, patientId1, patientId2)); searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); ourLog.info("Search SQL:\n{}", searchSql); @@ -1357,7 +1523,7 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { map.setLoadSynchronous(true); results = myPatientDao.search(map); ids = toUnqualifiedVersionlessIds(results); - assertThat(ids, Matchers.contains(patientIdNull, patientId1, patientId2)); + assertThat(ids, contains(patientIdNull, patientId1, patientId2)); searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); ourLog.info("Search SQL:\n{}", searchSql); @@ -1371,9 +1537,9 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { public void testSearch_DateParam_SearchSpecificPartitions() { myPartitionSettings.setIncludePartitionInSearchHashes(false); - IIdType patientIdNull = createPatient(withPartition(null), withBirthdate("2020-04-20")); + createPatient(withPartition(null), withBirthdate("2020-04-20")); IIdType patientId1 = createPatient(withPartition(1), withBirthdate("2020-04-20")); - IIdType patientId2 = createPatient(withPartition(2), withBirthdate("2020-04-20")); + createPatient(withPartition(2), withBirthdate("2020-04-20")); createPatient(withPartition(null), withBirthdate("2021-04-20")); createPatient(withPartition(1), withBirthdate("2021-04-20")); createPatient(withPartition(2), withBirthdate("2021-04-20")); @@ -1390,7 +1556,7 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { IBundleProvider results = myPatientDao.search(map); myCaptureQueriesListener.logSelectQueriesForCurrentThread(); List ids = toUnqualifiedVersionlessIds(results); - assertThat(ids, Matchers.contains(patientId1)); + assertThat(ids, contains(patientId1)); String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); ourLog.info("Search SQL:\n{}", searchSql); @@ -1406,7 +1572,7 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { map.setLoadSynchronous(true); results = myPatientDao.search(map); ids = toUnqualifiedVersionlessIds(results); - assertThat(ids, Matchers.contains(patientId1)); + assertThat(ids, contains(patientId1)); searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); ourLog.info("Search SQL:\n{}", searchSql); @@ -1422,7 +1588,7 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { map.setLoadSynchronous(true); results = myPatientDao.search(map); ids = toUnqualifiedVersionlessIds(results); - assertThat(ids, Matchers.contains(patientId1)); + assertThat(ids, contains(patientId1)); searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); ourLog.info("Search SQL:\n{}", searchSql); @@ -1438,7 +1604,7 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { map.setLoadSynchronous(true); results = myPatientDao.search(map); ids = toUnqualifiedVersionlessIds(results); - assertThat(ids, Matchers.contains(patientId1)); + assertThat(ids, contains(patientId1)); searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); ourLog.info("Search SQL:\n{}", searchSql); @@ -1453,8 +1619,8 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { myPartitionSettings.setIncludePartitionInSearchHashes(false); IIdType patientIdNull = createPatient(withPartition(null), withBirthdate("2020-04-20")); - IIdType patientId1 = createPatient(withPartition(1), withBirthdate("2020-04-20")); - IIdType patientId2 = createPatient(withPartition(2), withBirthdate("2020-04-20")); + createPatient(withPartition(1), withBirthdate("2020-04-20")); + createPatient(withPartition(2), withBirthdate("2020-04-20")); createPatient(withPartition(null), withBirthdate("2021-04-20")); createPatient(withPartition(1), withBirthdate("2021-04-20")); createPatient(withPartition(2), withBirthdate("2021-04-20")); @@ -1468,7 +1634,7 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { map.setLoadSynchronous(true); IBundleProvider results = myPatientDao.search(map); List ids = toUnqualifiedVersionlessIds(results); - assertThat(ids, Matchers.contains(patientIdNull)); + assertThat(ids, contains(patientIdNull)); String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); ourLog.info("Search SQL:\n{}", searchSql); @@ -1484,7 +1650,7 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { map.setLoadSynchronous(true); results = myPatientDao.search(map); ids = toUnqualifiedVersionlessIds(results); - assertThat(ids, Matchers.contains(patientIdNull)); + assertThat(ids, contains(patientIdNull)); searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); ourLog.info("Search SQL:\n{}", searchSql); @@ -1500,7 +1666,7 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { map.setLoadSynchronous(true); results = myPatientDao.search(map); ids = toUnqualifiedVersionlessIds(results); - assertThat(ids, Matchers.contains(patientIdNull)); + assertThat(ids, contains(patientIdNull)); searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); ourLog.info("Search SQL:\n{}", searchSql); @@ -1516,7 +1682,7 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { map.setLoadSynchronous(true); results = myPatientDao.search(map); ids = toUnqualifiedVersionlessIds(results); - assertThat(ids, Matchers.contains(patientIdNull)); + assertThat(ids, contains(patientIdNull)); searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); ourLog.info("Search SQL:\n{}", searchSql); @@ -1581,7 +1747,7 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { map.setLoadSynchronous(true); IBundleProvider results = myPatientDao.search(map); List ids = toUnqualifiedVersionlessIds(results); - assertThat(ids, Matchers.contains(patientIdNull, patientId1, patientId2)); + assertThat(ids, contains(patientIdNull, patientId1, patientId2)); String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); ourLog.info("Search SQL:\n{}", searchSql); @@ -1603,7 +1769,7 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { map.setLoadSynchronous(true); IBundleProvider results = myPatientDao.search(map); List ids = toUnqualifiedVersionlessIds(results); - assertThat(ids, Matchers.contains(patientIdNull)); + assertThat(ids, contains(patientIdNull)); String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); ourLog.info("Search SQL:\n{}", searchSql); @@ -1628,7 +1794,7 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { IBundleProvider results = myPatientDao.search(map); List ids = toUnqualifiedVersionlessIds(results); myCaptureQueriesListener.logSelectQueriesForCurrentThread(); - assertThat(ids, Matchers.contains(patientId1)); + assertThat(ids, contains(patientId1)); String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); ourLog.info("Search SQL:\n{}", searchSql); @@ -1636,6 +1802,73 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { assertEquals(1, StringUtils.countMatches(searchSql, "SP_VALUE_NORMALIZED")); } + @Test + public void testSearch_StringParam_SearchMultiplePartitions() { + IIdType patientIdNull = createPatient(withPartition(null), withFamily("FAMILY")); + IIdType patientId1 = createPatient(withPartition(1), withFamily("FAMILY")); + IIdType patientId2 = createPatient(withPartition(2), withFamily("FAMILY")); + createPatient(withPartition(3), withFamily("FAMILY")); + + createPatient(withPartition(null), withFamily("BLAH")); + createPatient(withPartition(1), withFamily("BLAH")); + createPatient(withPartition(2), withFamily("BLAH")); + createPatient(withPartition(3), withFamily("BLAH")); + + + SearchParameterMap map = new SearchParameterMap(); + map.add(Patient.SP_FAMILY, new StringParam("FAMILY")); + map.setLoadSynchronous(true); + + // Match two partitions + { + addReadPartition(1, 2); + + myCaptureQueriesListener.clear(); + IBundleProvider results = myPatientDao.search(map); + List ids = toUnqualifiedVersionlessIds(results); + assertThat(ids.toString(), ids, Matchers.containsInAnyOrder(patientId1, patientId2)); + + ourLog.info("Search SQL:\n{}", myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true)); + String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false); + assertThat(searchSql, containsString("PARTITION_ID IN ('1','2')")); + assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID")); + } + + // Match two partitions including null + { + addReadPartition(1, null); + + myCaptureQueriesListener.clear(); + IBundleProvider results = myPatientDao.search(map); + List ids = toUnqualifiedVersionlessIds(results); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + assertThat(ids.toString(), ids, Matchers.containsInAnyOrder(patientId1, patientIdNull)); + + ourLog.info("Search SQL:\n{}", myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true)); + String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false); + assertThat(searchSql, containsString("PARTITION_ID IS NULL")); + assertThat(searchSql, containsString("PARTITION_ID IN ('1')")); + assertEquals(2, StringUtils.countMatches(searchSql, "PARTITION_ID")); + } + } + + @Test + public void testSearch_StringParam_SearchMultiplePartitions_IncludePartitionInHashes() { + myPartitionSettings.setIncludePartitionInSearchHashes(true); + + SearchParameterMap map = new SearchParameterMap(); + map.add(Patient.SP_FAMILY, new StringParam("FAMILY")); + map.setLoadSynchronous(true); + + addReadPartition(1, 2); + try { + myPatientDao.search(map); + fail(); + } catch (InternalErrorException e) { + assertEquals("Can not search multiple partitions when partitions are included in search hashes", e.getMessage()); + } + } + @Test public void testSearch_StringParam_SearchAllPartitions_IncludePartitionInHashes() { myPartitionSettings.setIncludePartitionInSearchHashes(true); @@ -1671,7 +1904,7 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { map.setLoadSynchronous(true); IBundleProvider results = myPatientDao.search(map); List ids = toUnqualifiedVersionlessIds(results); - assertThat(ids, Matchers.contains(patientIdNull)); + assertThat(ids, contains(patientIdNull)); String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); ourLog.info("Search SQL:\n{}", searchSql); @@ -1698,7 +1931,7 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { IBundleProvider results = myPatientDao.search(map); List ids = toUnqualifiedVersionlessIds(results); myCaptureQueriesListener.logSelectQueriesForCurrentThread(); - assertThat(ids, Matchers.contains(patientId1)); + assertThat(ids, contains(patientId1)); String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); ourLog.info("Search SQL:\n{}", searchSql); @@ -1722,7 +1955,7 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { map.setLoadSynchronous(true); IBundleProvider results = myPatientDao.search(map); List ids = toUnqualifiedVersionlessIds(results); - assertThat(ids, Matchers.contains(patientIdNull, patientId1, patientId2)); + assertThat(ids, contains(patientIdNull, patientId1, patientId2)); String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); ourLog.info("Search SQL:\n{}", searchSql); @@ -1739,7 +1972,7 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { map.setLoadSynchronous(true); results = myPatientDao.search(map); ids = toUnqualifiedVersionlessIds(results); - assertThat(ids, Matchers.contains(patientIdNull, patientId1)); + assertThat(ids, contains(patientIdNull, patientId1)); searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); ourLog.info("Search SQL:\n{}", searchSql); @@ -1771,7 +2004,7 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID IS NULL")); assertEquals(1, StringUtils.countMatches(searchSql, "TAG_SYSTEM = 'http://system'")); - assertThat(ids.toString(), ids, Matchers.contains(patientIdNull)); + assertThat(ids.toString(), ids, contains(patientIdNull)); } @Test @@ -1791,7 +2024,7 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { map.setLoadSynchronous(true); IBundleProvider results = myPatientDao.search(map); List ids = toUnqualifiedVersionlessIds(results); - assertThat(ids, Matchers.contains(patientId1)); + assertThat(ids, contains(patientId1)); String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); ourLog.info("Search SQL:\n{}", searchSql); @@ -1813,7 +2046,7 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { map.setLoadSynchronous(true); IBundleProvider results = myPatientDao.search(map); List ids = toUnqualifiedVersionlessIds(results); - assertThat(ids, Matchers.contains(patientIdNull, patientId1, patientId2)); + assertThat(ids, contains(patientIdNull, patientId1, patientId2)); String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); ourLog.info("Search SQL:\n{}", searchSql); @@ -1837,7 +2070,7 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { IBundleProvider results = myPatientDao.search(map); List ids = toUnqualifiedVersionlessIds(results); myCaptureQueriesListener.logSelectQueriesForCurrentThread(0); - assertThat(ids, Matchers.contains(patientId1)); + assertThat(ids, contains(patientId1)); String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); ourLog.info("Search SQL:\n{}", searchSql); @@ -1864,7 +2097,7 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { map.setLoadSynchronous(true); IBundleProvider results = myPatientDao.search(map); List ids = toUnqualifiedVersionlessIds(results); - assertThat(ids, Matchers.contains(patientIdNull, patientId1, patientId2)); + assertThat(ids, contains(patientIdNull, patientId1, patientId2)); String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); ourLog.info("Search SQL:\n{}", searchSql); @@ -1889,7 +2122,7 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { map.setLoadSynchronous(true); IBundleProvider results = myPatientDao.search(map); List ids = toUnqualifiedVersionlessIds(results); - assertThat(ids, Matchers.contains(patientId1)); + assertThat(ids, contains(patientId1)); String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); ourLog.info("Search SQL:\n{}", searchSql); @@ -1912,7 +2145,7 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { IBundleProvider results = myPatientDao.search(map); List ids = toUnqualifiedVersionlessIds(results); myCaptureQueriesListener.logSelectQueriesForCurrentThread(); - assertThat(ids, Matchers.contains(id)); + assertThat(ids, contains(id)); String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); ourLog.info("Search SQL:\n{}", searchSql); @@ -1935,7 +2168,7 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { IBundleProvider results = myPatientDao.search(map); List ids = toUnqualifiedVersionlessIds(results); myCaptureQueriesListener.logSelectQueriesForCurrentThread(); - assertThat(ids, Matchers.contains(id)); + assertThat(ids, contains(id)); String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); ourLog.info("Search SQL:\n{}", searchSql); @@ -1970,11 +2203,11 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { IBundleProvider results = myObservationDao.search(map); List ids = toUnqualifiedVersionlessIds(results); myCaptureQueriesListener.logSelectQueriesForCurrentThread(); - assertThat(ids, Matchers.contains(observationId)); + assertThat(ids, contains(observationId)); + ourLog.info("Search SQL:\n{}", myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true)); String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false); - ourLog.info("Search SQL:\n{}", searchSql); - assertEquals(1, StringUtils.countMatches(searchSql, "t0.PARTITION_ID = '1'"), searchSql); + assertEquals(1, StringUtils.countMatches(searchSql, "t0.PARTITION_ID IN ('1')"), searchSql); assertEquals(1, StringUtils.countMatches(searchSql, "t0.SRC_PATH = 'Observation.subject'"), searchSql); assertEquals(1, StringUtils.countMatches(searchSql, "t0.TARGET_RESOURCE_ID = '" + patientId.getIdPartAsLong() + "'"), searchSql); assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID"), searchSql); @@ -2000,14 +2233,14 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { IIdType observationId = createObservation(withPartition(null), withSubject(patientId)); addReadDefaultPartition(); - ; + myCaptureQueriesListener.clear(); SearchParameterMap map = new SearchParameterMap(); map.add(Observation.SP_SUBJECT, new ReferenceParam(patientId)); map.setLoadSynchronous(true); IBundleProvider results = myObservationDao.search(map); List ids = toUnqualifiedVersionlessIds(results); - assertThat(ids, Matchers.contains(observationId)); + assertThat(ids, contains(observationId)); String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false); ourLog.info("Search SQL:\n{}", searchSql); @@ -2044,11 +2277,11 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { IBundleProvider results = myObservationDao.search(map); List ids = toUnqualifiedVersionlessIds(results); myCaptureQueriesListener.logSelectQueriesForCurrentThread(); - assertThat(ids, Matchers.contains(observationId)); + assertThat(ids, contains(observationId)); - String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); - ourLog.info("Search SQL:\n{}", searchSql); - assertEquals(1, StringUtils.countMatches(searchSql, "forcedid0_.PARTITION_ID='1'"), searchSql); + ourLog.info("Search SQL:\n{}", myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true)); + String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false); + assertEquals(1, StringUtils.countMatches(searchSql, "forcedid0_.PARTITION_ID in ('1')"), searchSql); assertEquals(1, StringUtils.countMatches(searchSql, "and forcedid0_.RESOURCE_TYPE='Patient'"), searchSql); assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID"), searchSql); @@ -2080,10 +2313,10 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { map.setLoadSynchronous(true); IBundleProvider results = myObservationDao.search(map); List ids = toUnqualifiedVersionlessIds(results); - assertThat(ids, Matchers.contains(observationId)); String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); ourLog.info("Search SQL:\n{}", searchSql); + assertThat(ids, contains(observationId)); // FIXME: move up assertEquals(1, StringUtils.countMatches(searchSql, "forcedid0_.PARTITION_ID is null"), searchSql); assertEquals(1, StringUtils.countMatches(searchSql, "forcedid0_.RESOURCE_TYPE='Patient'"), searchSql); assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID"), searchSql); @@ -2129,7 +2362,7 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { IBundleProvider results = myPatientDao.history(id, null, null, mySrd); assertEquals(2, results.sizeOrThrowNpe()); List ids = toUnqualifiedIdValues(results); - assertThat(ids, Matchers.contains(id.withVersion("2").getValue(), id.withVersion("1").getValue())); + assertThat(ids, contains(id.withVersion("2").getValue(), id.withVersion("1").getValue())); assertEquals(4, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); @@ -2192,7 +2425,7 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { IBundleProvider results = myPatientDao.history(id, null, null, mySrd); assertEquals(2, results.sizeOrThrowNpe()); List ids = toUnqualifiedIdValues(results); - assertThat(ids, Matchers.contains(id.withVersion("2").getValue(), id.withVersion("1").getValue())); + assertThat(ids, contains(id.withVersion("2").getValue(), id.withVersion("1").getValue())); assertEquals(4, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); @@ -2233,7 +2466,7 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { IBundleProvider results = myPatientDao.history(id, null, null, mySrd); assertEquals(2, results.sizeOrThrowNpe()); List ids = toUnqualifiedIdValues(results); - assertThat(ids, Matchers.contains(id.withVersion("2").getValue(), id.withVersion("1").getValue())); + assertThat(ids, contains(id.withVersion("2").getValue(), id.withVersion("1").getValue())); } @Test @@ -2262,26 +2495,27 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { IBundleProvider results = mySystemDao.history(null, null, mySrd); assertEquals(2, results.sizeOrThrowNpe()); List ids = toUnqualifiedIdValues(results); - assertThat(ids, Matchers.contains(id1B.withVersion("1").getValue(), id1A.withVersion("1").getValue())); + assertThat(ids, contains(id1B.withVersion("1").getValue(), id1A.withVersion("1").getValue())); assertEquals(3, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); // Count - String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); - ourLog.info("SQL:{}", searchSql); - assertEquals(1, countMatches(searchSql, "count(")); - assertEquals(1, countMatches(searchSql, "PARTITION_ID='1'")); + ourLog.info("SQL:{}", myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true)); + String sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false).toUpperCase(); + assertEquals(1, countMatches(sql, "COUNT("), sql); + assertEquals(1, countMatches(sql, "PARTITION_ID IN ('1')"), sql); // Fetch history - searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(1).getSql(true, true); - ourLog.info("SQL:{}", searchSql); - assertEquals(1, countMatches(searchSql, "PARTITION_ID='1'")); + sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(1).getSql(true, false).toUpperCase(); + ourLog.info("SQL:{}", sql); + assertEquals(1, countMatches(sql, "PARTITION_ID IN ('1')"), sql); // Fetch history resource - searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(2).getSql(true, true); - ourLog.info("SQL:{}", searchSql); - assertEquals(0, countMatches(searchSql, "PARTITION_ID="), searchSql.replace(" ", "").toUpperCase()); - assertEquals(0, countMatches(searchSql, "PARTITION_IDIN"), searchSql.replace(" ", "").toUpperCase()); + sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(2).getSql(true, false); + sql = sql.replace(" ", "").toUpperCase(); + ourLog.info("SQL:{}", sql); + assertEquals(0, countMatches(sql, "PARTITION_ID="), sql); + assertEquals(0, countMatches(sql, "PARTITION_IDIN"), sql); } @Test @@ -2299,7 +2533,7 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { IBundleProvider results = mySystemDao.history(null, null, mySrd); assertEquals(2, results.sizeOrThrowNpe()); List ids = toUnqualifiedIdValues(results); - assertThat(ids, Matchers.contains(id1B.withVersion("1").getValue(), id1A.withVersion("1").getValue())); + assertThat(ids, contains(id1B.withVersion("1").getValue(), id1A.withVersion("1").getValue())); assertEquals(3, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); @@ -2320,6 +2554,42 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { assertEquals(0, countMatches(searchSql, "PARTITION_IDIN"), searchSql.replace(" ", "").toUpperCase()); } + @Test + public void testHistory_Server_MultiplePartitions() { + String idNull1 = createPatient(withPartition(null), withBirthdate("2020-01-01")).toUnqualifiedVersionless().getValue(); + sleepAtLeast(10); + String idNull2 = createPatient(withPartition(null), withBirthdate("2020-01-01")).toUnqualifiedVersionless().getValue(); + sleepAtLeast(10); + String id21 = createPatient(withPartition(2), withBirthdate("2020-01-01")).toUnqualifiedVersionless().getValue(); + sleepAtLeast(10); + String id31 = createPatient(withPartition(3), withBirthdate("2020-01-01")).toUnqualifiedVersionless().getValue(); + sleepAtLeast(10); + String id22 = createPatient(withPartition(2), withBirthdate("2020-01-01")).toUnqualifiedVersionless().getValue(); + sleepAtLeast(10); + String id32 = createPatient(withPartition(3), withBirthdate("2020-01-01")).toUnqualifiedVersionless().getValue(); + + // Multiple Partitions + { + addReadPartition(2, null); + myCaptureQueriesListener.clear(); + IBundleProvider results = mySystemDao.history(null, null, mySrd); + assertEquals(4, results.sizeOrThrowNpe()); + List ids = toUnqualifiedVersionlessIdValues(results); + assertThat(ids, contains(id22, id21, idNull2, idNull1)); + } + + // Multiple Partitions With Null + { + addReadPartition(2, 3); + myCaptureQueriesListener.clear(); + IBundleProvider results = mySystemDao.history(null, null, mySrd); + assertEquals(4, results.sizeOrThrowNpe()); + List ids = toUnqualifiedVersionlessIdValues(results); + assertThat(ids, contains(id32, id22, id31, id21)); + } + + } + @Test public void testHistory_Type_AllPartitions() { addReadAllPartitions(); @@ -2346,25 +2616,25 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { IBundleProvider results = myPatientDao.history(null, null, mySrd); assertEquals(2, results.sizeOrThrowNpe()); List ids = toUnqualifiedIdValues(results); - assertThat(ids, Matchers.contains(id1B.withVersion("1").getValue(), id1A.withVersion("1").getValue())); + assertThat(ids, contains(id1B.withVersion("1").getValue(), id1A.withVersion("1").getValue())); assertEquals(3, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); // Count - String sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); + String sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false).toUpperCase(); ourLog.info("SQL:{}", sql); - assertEquals(1, countMatches(sql, "count(")); - assertEquals(1, countMatches(sql, "PARTITION_ID='1'")); + assertEquals(1, countMatches(sql, "COUNT("), sql); + assertEquals(1, countMatches(sql, "PARTITION_ID IN ('1')"), sql); // Fetch history resources - sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(1).getSql(true, true); + sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(1).getSql(true, false).toUpperCase(); ourLog.info("SQL:{}", sql); - assertEquals(1, countMatches(sql, "PARTITION_ID='1'")); + assertEquals(1, countMatches(sql, "PARTITION_ID IN ('1')"), sql); // Resolve forced ID - sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(2).getSql(true, true); + sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(2).getSql(true, false).toUpperCase(); ourLog.info("SQL:{}", sql); - assertEquals(0, countMatches(sql, "PARTITION_ID='1'")); + assertEquals(0, countMatches(sql, "PARTITION_ID IN ('1')"), sql); } @@ -2383,7 +2653,7 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { IBundleProvider results = myPatientDao.history(null, null, mySrd); assertEquals(2, results.sizeOrThrowNpe()); List ids = toUnqualifiedIdValues(results); - assertThat(ids, Matchers.contains(id1B.withVersion("1").getValue(), id1A.withVersion("1").getValue())); + assertThat(ids, contains(id1B.withVersion("1").getValue(), id1A.withVersion("1").getValue())); myCaptureQueriesListener.logSelectQueriesForCurrentThread(); assertEquals(3, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); @@ -2414,8 +2684,8 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { verify(interceptor, times(1)).invoke(eq(Pointcut.STORAGE_PARTITION_SELECTED), captor.capture()); RequestPartitionId partitionId = captor.getValue().get(RequestPartitionId.class); - assertEquals(1, partitionId.getPartitionId().intValue()); - assertEquals("PART-1", partitionId.getPartitionName()); + assertEquals(1, partitionId.getPartitionIds().get(0).intValue()); + assertEquals("PART-1", partitionId.getPartitionNames().get(0)); } finally { myInterceptorRegistry.unregisterInterceptor(interceptor); @@ -2471,9 +2741,15 @@ public class PartitioningSqlR4Test extends BaseJpaR4SystemTest { myPartitionInterceptor.addCreatePartition(requestPartitionId); } - private void addReadPartition(Integer thePartitionId) { + private void addReadPartition(Integer... thePartitionId) { Validate.notNull(thePartitionId); - myPartitionInterceptor.addReadPartition(RequestPartitionId.fromPartitionId(thePartitionId, null)); + myPartitionInterceptor.addReadPartition(RequestPartitionId.fromPartitionIds(thePartitionId)); + } + + private void addReadPartitions(String... thePartitionNames) { + Validate.notNull(thePartitionNames); + Validate.isTrue(thePartitionNames.length > 0); + myPartitionInterceptor.addReadPartition(RequestPartitionId.fromPartitionNames(thePartitionNames)); } private void addReadDefaultPartition() { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/partition/PartitionSettingsSvcImplTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/partition/PartitionSettingsSvcImplTest.java index 6bf1c707163..56b66281397 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/partition/PartitionSettingsSvcImplTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/partition/PartitionSettingsSvcImplTest.java @@ -50,18 +50,6 @@ public class PartitionSettingsSvcImplTest extends BaseJpaR4Test { } - @Test - public void testDeletePartition_TryToDeleteDefault() { - - try { - myPartitionConfigSvc.deletePartition(0); - fail(); - } catch (InvalidRequestException e) { - assertEquals("Can not delete default partition", e.getMessage()); - } - - } - @Test public void testUpdatePartition_TryToUseExistingName() { @@ -92,14 +80,14 @@ public class PartitionSettingsSvcImplTest extends BaseJpaR4Test { @Test public void testUpdatePartition_TryToRenameDefault() { PartitionEntity partition = new PartitionEntity(); - partition.setId(0); + partition.setId(null); partition.setName("NAME123"); partition.setDescription("A description"); try { myPartitionConfigSvc.updatePartition(partition); fail(); } catch (InvalidRequestException e) { - assertEquals("Can not rename default partition", e.getMessage()); + assertEquals("Partition must have an ID and a Name", e.getMessage()); } } @@ -141,21 +129,6 @@ public class PartitionSettingsSvcImplTest extends BaseJpaR4Test { } - @Test - public void testCreatePartition_0Blocked() { - PartitionEntity partition = new PartitionEntity(); - partition.setId(0); - partition.setName("NAME123"); - partition.setDescription("A description"); - try { - myPartitionConfigSvc.createPartition(partition); - fail(); - } catch (InvalidRequestException e) { - assertEquals("Can not create a partition with ID 0 (this is a reserved value)", e.getMessage()); - } - - } - @Test public void testUpdatePartition_UnknownPartitionBlocked() { PartitionEntity partition = new PartitionEntity(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseMultitenantResourceProviderR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseMultitenantResourceProviderR4Test.java index c85c4315dbd..15905dcc611 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseMultitenantResourceProviderR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseMultitenantResourceProviderR4Test.java @@ -27,7 +27,7 @@ import java.util.List; import java.util.function.Consumer; import java.util.function.Supplier; -import static ca.uhn.fhir.jpa.partition.PartitionLookupSvcImpl.DEFAULT_PERSISTED_PARTITION_NAME; +import static ca.uhn.fhir.jpa.model.util.JpaConstants.DEFAULT_PARTITION_NAME; public abstract class BaseMultitenantResourceProviderR4Test extends BaseResourceProviderR4Test implements ITestDataBuilder { @@ -88,7 +88,7 @@ public abstract class BaseMultitenantResourceProviderR4Test extends BaseResource private void createTenants() { - myTenantClientInterceptor.setTenantId(DEFAULT_PERSISTED_PARTITION_NAME); + myTenantClientInterceptor.setTenantId(DEFAULT_PARTITION_NAME); myClient .operation() diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java index 9eb61dabdf2..e8706afa599 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java @@ -3,6 +3,7 @@ package ca.uhn.fhir.jpa.provider.r4; import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc; +import ca.uhn.fhir.jpa.dao.data.IPartitionDao; import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test; import ca.uhn.fhir.jpa.provider.DiffProvider; import ca.uhn.fhir.jpa.provider.GraphQLProvider; @@ -65,15 +66,15 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test { protected static Server ourServer; private static DatabaseBackedPagingProvider ourPagingProvider; private static GenericWebApplicationContext ourWebApplicationContext; - private static SubscriptionMatcherInterceptor ourSubscriptionMatcherInterceptor; protected IGenericClient myClient; @Autowired protected SubscriptionLoader mySubscriptionLoader; @Autowired protected DaoRegistry myDaoRegistry; + @Autowired + protected IPartitionDao myPartitionDao; ResourceCountCache myResourceCountsCache; private TerminologyUploaderProvider myTerminologyUploaderProvider; - private boolean ourRestHookSubscriptionInterceptorRequested; public BaseResourceProviderR4Test() { super(); @@ -163,7 +164,7 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test { WebApplicationContext wac = WebApplicationContextUtils.getWebApplicationContext(subsServletHolder.getServlet().getServletConfig().getServletContext()); myValidationSupport = wac.getBean(IValidationSupport.class); mySearchCoordinatorSvc = wac.getBean(ISearchCoordinatorSvc.class); - ourSubscriptionMatcherInterceptor = wac.getBean(SubscriptionMatcherInterceptor.class); + SubscriptionMatcherInterceptor ourSubscriptionMatcherInterceptor = wac.getBean(SubscriptionMatcherInterceptor.class); confProvider.setSearchParamRegistry(ourSearchParamRegistry); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/MultitenantServerR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/MultitenantServerR4Test.java index 21300f2adf2..24d9d4d1fcc 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/MultitenantServerR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/MultitenantServerR4Test.java @@ -1,5 +1,8 @@ package ca.uhn.fhir.jpa.provider.r4; +import ca.uhn.fhir.jpa.entity.PartitionEntity; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.util.JpaConstants; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.test.utilities.ITestDataBuilder; import org.hl7.fhir.instance.model.api.IIdType; @@ -15,6 +18,8 @@ import java.util.Date; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -31,19 +36,34 @@ public class MultitenantServerR4Test extends BaseMultitenantResourceProviderR4Te } @Test - public void testCreateAndRead() { + public void testCreateAndRead_NamedTenant() { // Create patients IIdType idA = createPatient(withTenant(TENANT_A), withActiveTrue()); createPatient(withTenant(TENANT_B), withActiveFalse()); + runInTransaction(() -> { + PartitionEntity partition = myPartitionDao.findForName(TENANT_A).orElseThrow(() -> new IllegalStateException()); + ResourceTable resourceTable = myResourceTableDao.findById(idA.getIdPartAsLong()).orElseThrow(() -> new IllegalStateException()); + assertEquals(partition.getId(), resourceTable.getPartitionId().getPartitionId()); + }); + // Now read back myTenantClientInterceptor.setTenantId(TENANT_A); Patient response = myClient.read().resource(Patient.class).withId(idA).execute(); assertTrue(response.getActive()); + // Update resource (should remain in correct partition) + + createPatient(withActiveFalse(), withId(idA)); + + // Now read back + + response = myClient.read().resource(Patient.class).withId(idA.withVersion("2")).execute(); + assertFalse(response.getActive()); + myTenantClientInterceptor.setTenantId(TENANT_B); try { myClient.read().resource(Patient.class).withId(idA).execute(); @@ -53,6 +73,47 @@ public class MultitenantServerR4Test extends BaseMultitenantResourceProviderR4Te } } + @Test + public void testCreateAndRead_DefaultTenant() { + + // Create patients + + IIdType idA = createPatient(withTenant(JpaConstants.DEFAULT_PARTITION_NAME), withActiveTrue()); + createPatient(withTenant(TENANT_B), withActiveFalse()); + + runInTransaction(() -> { + ResourceTable resourceTable = myResourceTableDao.findById(idA.getIdPartAsLong()).orElseThrow(() -> new IllegalStateException()); + assertNull(resourceTable.getPartitionId()); + }); + + + // Now read back + + myTenantClientInterceptor.setTenantId(JpaConstants.DEFAULT_PARTITION_NAME); + Patient response = myClient.read().resource(Patient.class).withId(idA).execute(); + assertTrue(response.getActive()); + + // Update resource (should remain in correct partition) + + createPatient(withActiveFalse(), withId(idA)); + + // Now read back + + response = myClient.read().resource(Patient.class).withId(idA.withVersion("2")).execute(); + assertFalse(response.getActive()); + + // Try reading from wrong partition + + myTenantClientInterceptor.setTenantId(TENANT_B); + try { + myClient.read().resource(Patient.class).withId(idA).execute(); + fail(); + } catch (ResourceNotFoundException e) { + // good + } + } + + @Test public void testCreate_InvalidTenant() { diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java index 5b380224189..d8700b16240 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java @@ -20,6 +20,7 @@ package ca.uhn.fhir.jpa.migrate.tasks; * #L% */ +import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.entity.EmpiLink; import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion; import ca.uhn.fhir.jpa.entity.TermConceptMap; @@ -748,7 +749,7 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { .withColumns("HASH_IDENTITY", "SP_LATITUDE", "SP_LONGITUDE"); spidxCoords .addTask(new CalculateHashesTask(VersionEnum.V3_5_0, "20180903.5") - .addCalculator("HASH_IDENTITY", t -> BaseResourceIndexedSearchParam.calculateHashIdentity(new PartitionSettings(), null, t.getResourceType(), t.getString("SP_NAME"))) + .addCalculator("HASH_IDENTITY", t -> BaseResourceIndexedSearchParam.calculateHashIdentity(new PartitionSettings(), RequestPartitionId.defaultPartition(), t.getResourceType(), t.getString("SP_NAME"))) .setColumnName("HASH_IDENTITY") ); } @@ -771,7 +772,7 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { .dropIndex("20180903.9", "IDX_SP_DATE"); spidxDate .addTask(new CalculateHashesTask(VersionEnum.V3_5_0, "20180903.10") - .addCalculator("HASH_IDENTITY", t -> BaseResourceIndexedSearchParam.calculateHashIdentity(new PartitionSettings(), null, t.getResourceType(), t.getString("SP_NAME"))) + .addCalculator("HASH_IDENTITY", t -> BaseResourceIndexedSearchParam.calculateHashIdentity(new PartitionSettings(), RequestPartitionId.defaultPartition(), t.getResourceType(), t.getString("SP_NAME"))) .setColumnName("HASH_IDENTITY") ); } @@ -792,7 +793,7 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { .withColumns("HASH_IDENTITY", "SP_VALUE"); spidxNumber .addTask(new CalculateHashesTask(VersionEnum.V3_5_0, "20180903.14") - .addCalculator("HASH_IDENTITY", t -> BaseResourceIndexedSearchParam.calculateHashIdentity(new PartitionSettings(), null, t.getResourceType(), t.getString("SP_NAME"))) + .addCalculator("HASH_IDENTITY", t -> BaseResourceIndexedSearchParam.calculateHashIdentity(new PartitionSettings(), RequestPartitionId.defaultPartition(), t.getResourceType(), t.getString("SP_NAME"))) .setColumnName("HASH_IDENTITY") ); } @@ -829,9 +830,9 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { .withColumns("HASH_IDENTITY_SYS_UNITS", "SP_VALUE"); spidxQuantity .addTask(new CalculateHashesTask(VersionEnum.V3_5_0, "20180903.22") - .addCalculator("HASH_IDENTITY", t -> BaseResourceIndexedSearchParam.calculateHashIdentity(new PartitionSettings(), null, t.getResourceType(), t.getString("SP_NAME"))) - .addCalculator("HASH_IDENTITY_AND_UNITS", t -> ResourceIndexedSearchParamQuantity.calculateHashUnits(new PartitionSettings(), null, t.getResourceType(), t.getString("SP_NAME"), t.getString("SP_UNITS"))) - .addCalculator("HASH_IDENTITY_SYS_UNITS", t -> ResourceIndexedSearchParamQuantity.calculateHashSystemAndUnits(new PartitionSettings(), null, t.getResourceType(), t.getString("SP_NAME"), t.getString("SP_SYSTEM"), t.getString("SP_UNITS"))) + .addCalculator("HASH_IDENTITY", t -> BaseResourceIndexedSearchParam.calculateHashIdentity(new PartitionSettings(), RequestPartitionId.defaultPartition(), t.getResourceType(), t.getString("SP_NAME"))) + .addCalculator("HASH_IDENTITY_AND_UNITS", t -> ResourceIndexedSearchParamQuantity.calculateHashUnits(new PartitionSettings(), RequestPartitionId.defaultPartition(), t.getResourceType(), t.getString("SP_NAME"), t.getString("SP_UNITS"))) + .addCalculator("HASH_IDENTITY_SYS_UNITS", t -> ResourceIndexedSearchParamQuantity.calculateHashSystemAndUnits(new PartitionSettings(), RequestPartitionId.defaultPartition(), t.getResourceType(), t.getString("SP_NAME"), t.getString("SP_SYSTEM"), t.getString("SP_UNITS"))) .setColumnName("HASH_IDENTITY") ); } @@ -861,8 +862,8 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { spidxString .addTask(new CalculateHashesTask(VersionEnum.V3_5_0, "20180903.28") .setColumnName("HASH_NORM_PREFIX") - .addCalculator("HASH_NORM_PREFIX", t -> ResourceIndexedSearchParamString.calculateHashNormalized(new PartitionSettings(), null, new ModelConfig(), t.getResourceType(), t.getString("SP_NAME"), t.getString("SP_VALUE_NORMALIZED"))) - .addCalculator("HASH_EXACT", t -> ResourceIndexedSearchParamString.calculateHashExact(new PartitionSettings(), null, t.getResourceType(), t.getParamName(), t.getString("SP_VALUE_EXACT"))) + .addCalculator("HASH_NORM_PREFIX", t -> ResourceIndexedSearchParamString.calculateHashNormalized(new PartitionSettings(), RequestPartitionId.defaultPartition(), new ModelConfig(), t.getResourceType(), t.getString("SP_NAME"), t.getString("SP_VALUE_NORMALIZED"))) + .addCalculator("HASH_EXACT", t -> ResourceIndexedSearchParamString.calculateHashExact(new PartitionSettings(), (ca.uhn.fhir.jpa.model.entity.PartitionablePartitionId) null, t.getResourceType(), t.getParamName(), t.getString("SP_VALUE_EXACT"))) ); } @@ -909,10 +910,10 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { spidxToken .addTask(new CalculateHashesTask(VersionEnum.V3_5_0, "20180903.39") .setColumnName("HASH_IDENTITY") - .addCalculator("HASH_IDENTITY", t -> BaseResourceIndexedSearchParam.calculateHashIdentity(new PartitionSettings(), null, t.getResourceType(), t.getString("SP_NAME"))) - .addCalculator("HASH_SYS", t -> ResourceIndexedSearchParamToken.calculateHashSystem(new PartitionSettings(), null, t.getResourceType(), t.getParamName(), t.getString("SP_SYSTEM"))) - .addCalculator("HASH_SYS_AND_VALUE", t -> ResourceIndexedSearchParamToken.calculateHashSystemAndValue(new PartitionSettings(), null, t.getResourceType(), t.getParamName(), t.getString("SP_SYSTEM"), t.getString("SP_VALUE"))) - .addCalculator("HASH_VALUE", t -> ResourceIndexedSearchParamToken.calculateHashValue(new PartitionSettings(), null, t.getResourceType(), t.getParamName(), t.getString("SP_VALUE"))) + .addCalculator("HASH_IDENTITY", t -> BaseResourceIndexedSearchParam.calculateHashIdentity(new PartitionSettings(), RequestPartitionId.defaultPartition(), t.getResourceType(), t.getString("SP_NAME"))) + .addCalculator("HASH_SYS", t -> ResourceIndexedSearchParamToken.calculateHashSystem(new PartitionSettings(), RequestPartitionId.defaultPartition(), t.getResourceType(), t.getParamName(), t.getString("SP_SYSTEM"))) + .addCalculator("HASH_SYS_AND_VALUE", t -> ResourceIndexedSearchParamToken.calculateHashSystemAndValue(new PartitionSettings(), RequestPartitionId.defaultPartition(), t.getResourceType(), t.getParamName(), t.getString("SP_SYSTEM"), t.getString("SP_VALUE"))) + .addCalculator("HASH_VALUE", t -> ResourceIndexedSearchParamToken.calculateHashValue(new PartitionSettings(), RequestPartitionId.defaultPartition(), t.getResourceType(), t.getParamName(), t.getString("SP_VALUE"))) ); } @@ -939,8 +940,8 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { spidxUri .addTask(new CalculateHashesTask(VersionEnum.V3_5_0, "20180903.44") .setColumnName("HASH_IDENTITY") - .addCalculator("HASH_IDENTITY", t -> BaseResourceIndexedSearchParam.calculateHashIdentity(new PartitionSettings(), null, t.getResourceType(), t.getString("SP_NAME"))) - .addCalculator("HASH_URI", t -> ResourceIndexedSearchParamUri.calculateHashUri(new PartitionSettings(), null, t.getResourceType(), t.getString("SP_NAME"), t.getString("SP_URI"))) + .addCalculator("HASH_IDENTITY", t -> BaseResourceIndexedSearchParam.calculateHashIdentity(new PartitionSettings(), (RequestPartitionId)null, t.getResourceType(), t.getString("SP_NAME"))) + .addCalculator("HASH_URI", t -> ResourceIndexedSearchParamUri.calculateHashUri(new PartitionSettings(), (RequestPartitionId)null, t.getResourceType(), t.getString("SP_NAME"), t.getString("SP_URI"))) ); } @@ -973,7 +974,7 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { Boolean present = columnToBoolean(t.get("SP_PRESENT")); String resType = (String) t.get("RES_TYPE"); String paramName = (String) t.get("PARAM_NAME"); - Long hash = SearchParamPresent.calculateHashPresence(new PartitionSettings(), null, resType, paramName, present); + Long hash = SearchParamPresent.calculateHashPresence(new PartitionSettings(), (RequestPartitionId)null, resType, paramName, present); consolidateSearchParamPresenceIndexesTask.executeSql("HFJ_RES_PARAM_PRESENT", "update HFJ_RES_PARAM_PRESENT set HASH_PRESENCE = ? where PID = ?", hash, pid); }); version.addTask(consolidateSearchParamPresenceIndexesTask); diff --git a/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/ArbitrarySqlTaskTest.java b/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/ArbitrarySqlTaskTest.java index 69af82ef359..c6bfb6d712d 100644 --- a/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/ArbitrarySqlTaskTest.java +++ b/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/ArbitrarySqlTaskTest.java @@ -1,5 +1,6 @@ package ca.uhn.fhir.jpa.migrate.taskdef; +import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.migrate.tasks.api.BaseMigrationTasks; import ca.uhn.fhir.jpa.model.config.PartitionSettings; import ca.uhn.fhir.jpa.model.entity.SearchParamPresent; @@ -42,7 +43,7 @@ public class ArbitrarySqlTaskTest extends BaseTest { Boolean present = (Boolean) t.get("SP_PRESENT"); String resType = (String) t.get("RES_TYPE"); String paramName = (String) t.get("PARAM_NAME"); - Long hash = SearchParamPresent.calculateHashPresence(new PartitionSettings(), null, resType, paramName, present); + Long hash = SearchParamPresent.calculateHashPresence(new PartitionSettings(), RequestPartitionId.defaultPartition(), resType, paramName, present); task.executeSql("HFJ_RES_PARAM_PRESENT", "update HFJ_RES_PARAM_PRESENT set HASH_PRESENT = ? where PID = ?", hash, pid); }); diff --git a/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/CalculateHashesTest.java b/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/CalculateHashesTest.java index 83ed0bf8c40..b8e4d5ad721 100644 --- a/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/CalculateHashesTest.java +++ b/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/CalculateHashesTest.java @@ -1,5 +1,6 @@ package ca.uhn.fhir.jpa.migrate.taskdef; +import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.model.config.PartitionSettings; import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken; @@ -27,10 +28,10 @@ public class CalculateHashesTest extends BaseTest { CalculateHashesTask task = new CalculateHashesTask(VersionEnum.V3_5_0, "1"); task.setTableName("HFJ_SPIDX_TOKEN"); task.setColumnName("HASH_IDENTITY"); - task.addCalculator("HASH_IDENTITY", t -> BaseResourceIndexedSearchParam.calculateHashIdentity(new PartitionSettings(), null, t.getResourceType(), t.getString("SP_NAME"))); - task.addCalculator("HASH_SYS", t -> ResourceIndexedSearchParamToken.calculateHashSystem(new PartitionSettings(), null, t.getResourceType(), t.getParamName(), t.getString("SP_SYSTEM"))); - task.addCalculator("HASH_SYS_AND_VALUE", t -> ResourceIndexedSearchParamToken.calculateHashSystemAndValue(new PartitionSettings(), null, t.getResourceType(), t.getParamName(), t.getString("SP_SYSTEM"), t.getString("SP_VALUE"))); - task.addCalculator("HASH_VALUE", t -> ResourceIndexedSearchParamToken.calculateHashValue(new PartitionSettings(), null, t.getResourceType(), t.getParamName(), t.getString("SP_VALUE"))); + task.addCalculator("HASH_IDENTITY", t -> BaseResourceIndexedSearchParam.calculateHashIdentity(new PartitionSettings(), RequestPartitionId.defaultPartition(), t.getResourceType(), t.getString("SP_NAME"))); + task.addCalculator("HASH_SYS", t -> ResourceIndexedSearchParamToken.calculateHashSystem(new PartitionSettings(), RequestPartitionId.defaultPartition(), t.getResourceType(), t.getParamName(), t.getString("SP_SYSTEM"))); + task.addCalculator("HASH_SYS_AND_VALUE", t -> ResourceIndexedSearchParamToken.calculateHashSystemAndValue(new PartitionSettings(), RequestPartitionId.defaultPartition(), t.getResourceType(), t.getParamName(), t.getString("SP_SYSTEM"), t.getString("SP_VALUE"))); + task.addCalculator("HASH_VALUE", t -> ResourceIndexedSearchParamToken.calculateHashValue(new PartitionSettings(), RequestPartitionId.defaultPartition(), t.getResourceType(), t.getParamName(), t.getString("SP_VALUE"))); task.setBatchSize(1); getMigrator().addTask(task); @@ -77,10 +78,10 @@ public class CalculateHashesTest extends BaseTest { CalculateHashesTask task = new CalculateHashesTask(VersionEnum.V3_5_0, "1"); task.setTableName("HFJ_SPIDX_TOKEN"); task.setColumnName("HASH_IDENTITY"); - task.addCalculator("HASH_IDENTITY", t -> BaseResourceIndexedSearchParam.calculateHashIdentity(new PartitionSettings(), null, t.getResourceType(), t.getString("SP_NAME"))); - task.addCalculator("HASH_SYS", t -> ResourceIndexedSearchParamToken.calculateHashSystem(new PartitionSettings(), null, t.getResourceType(), t.getParamName(), t.getString("SP_SYSTEM"))); - task.addCalculator("HASH_SYS_AND_VALUE", t -> ResourceIndexedSearchParamToken.calculateHashSystemAndValue(new PartitionSettings(), null, t.getResourceType(), t.getParamName(), t.getString("SP_SYSTEM"), t.getString("SP_VALUE"))); - task.addCalculator("HASH_VALUE", t -> ResourceIndexedSearchParamToken.calculateHashValue(new PartitionSettings(), null, t.getResourceType(), t.getParamName(), t.getString("SP_VALUE"))); + task.addCalculator("HASH_IDENTITY", t -> BaseResourceIndexedSearchParam.calculateHashIdentity(new PartitionSettings(), RequestPartitionId.defaultPartition(), t.getResourceType(), t.getString("SP_NAME"))); + task.addCalculator("HASH_SYS", t -> ResourceIndexedSearchParamToken.calculateHashSystem(new PartitionSettings(), RequestPartitionId.defaultPartition(), t.getResourceType(), t.getParamName(), t.getString("SP_SYSTEM"))); + task.addCalculator("HASH_SYS_AND_VALUE", t -> ResourceIndexedSearchParamToken.calculateHashSystemAndValue(new PartitionSettings(), RequestPartitionId.defaultPartition(), t.getResourceType(), t.getParamName(), t.getString("SP_SYSTEM"), t.getString("SP_VALUE"))); + task.addCalculator("HASH_VALUE", t -> ResourceIndexedSearchParamToken.calculateHashValue(new PartitionSettings(), RequestPartitionId.defaultPartition(), t.getResourceType(), t.getParamName(), t.getString("SP_VALUE"))); task.setBatchSize(3); getMigrator().addTask(task); diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BasePartitionable.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BasePartitionable.java index a051701c06d..ccabff927bf 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BasePartitionable.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BasePartitionable.java @@ -22,7 +22,6 @@ package ca.uhn.fhir.jpa.model.entity; import ca.uhn.fhir.interceptor.model.RequestPartitionId; -import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.persistence.Column; import javax.persistence.Embedded; @@ -42,18 +41,18 @@ public class BasePartitionable implements Serializable { @Column(name = PartitionablePartitionId.PARTITION_ID, insertable = false, updatable = false, nullable = true) private Integer myPartitionIdValue; - @Nonnull - public RequestPartitionId getPartitionId() { - if (myPartitionId != null) { - return myPartitionId.toPartitionId(); - } else { - return RequestPartitionId.defaultPartition(); - } + @Nullable + public PartitionablePartitionId getPartitionId() { + return myPartitionId; + } + + public void setPartitionId(PartitionablePartitionId thePartitionId) { + myPartitionId = thePartitionId; } public void setPartitionId(@Nullable RequestPartitionId theRequestPartitionId) { if (theRequestPartitionId != null) { - myPartitionId = new PartitionablePartitionId(theRequestPartitionId.getPartitionId(), theRequestPartitionId.getPartitionDate()); + myPartitionId = new PartitionablePartitionId(theRequestPartitionId.getFirstPartitionIdOrNull(), theRequestPartitionId.getPartitionDate()); } else { myPartitionId = null; } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseResourceIndexedSearchParam.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseResourceIndexedSearchParam.java index 6414663fdab..af518ae2723 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseResourceIndexedSearchParam.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseResourceIndexedSearchParam.java @@ -20,10 +20,12 @@ package ca.uhn.fhir.jpa.model.entity; * #L% */ +import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.model.config.PartitionSettings; import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.util.UrlUtil; import com.google.common.base.Charsets; import com.google.common.hash.HashCode; @@ -179,6 +181,11 @@ public abstract class BaseResourceIndexedSearchParam extends BaseResourceIndex { return myModelConfig; } + public static long calculateHashIdentity(PartitionSettings thePartitionSettings, PartitionablePartitionId theRequestPartitionId, String theResourceType, String theParamName) { + RequestPartitionId requestPartitionId = PartitionablePartitionId.toRequestPartitionId(theRequestPartitionId); + return calculateHashIdentity(thePartitionSettings, requestPartitionId, theResourceType, theParamName); + } + public static long calculateHashIdentity(PartitionSettings thePartitionSettings, RequestPartitionId theRequestPartitionId, String theResourceType, String theParamName) { return hash(thePartitionSettings, theRequestPartitionId, theResourceType, theParamName); } @@ -190,8 +197,12 @@ public abstract class BaseResourceIndexedSearchParam extends BaseResourceIndex { Hasher hasher = HASH_FUNCTION.newHasher(); if (thePartitionSettings.isPartitioningEnabled() && thePartitionSettings.isIncludePartitionInSearchHashes() && theRequestPartitionId != null) { - if (theRequestPartitionId.getPartitionId() != null) { - hasher.putInt(theRequestPartitionId.getPartitionId()); + if (theRequestPartitionId.getPartitionIds().size() > 1) { + throw new InternalErrorException("Can not search multiple partitions when partitions are included in search hashes"); + } + Integer partitionId = theRequestPartitionId.getFirstPartitionIdOrNull(); + if (partitionId != null) { + hasher.putInt(partitionId); } } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/IBaseResourceEntity.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/IBaseResourceEntity.java index aac0a67b8a6..0bea6b4b237 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/IBaseResourceEntity.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/IBaseResourceEntity.java @@ -25,6 +25,7 @@ import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.InstantDt; +import javax.annotation.Nullable; import java.util.Date; public interface IBaseResourceEntity { @@ -51,5 +52,6 @@ public interface IBaseResourceEntity { boolean isHasTags(); - RequestPartitionId getPartitionId(); + @Nullable + PartitionablePartitionId getPartitionId(); } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/PartitionablePartitionId.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/PartitionablePartitionId.java index 8ea9849719b..d0feba30ac2 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/PartitionablePartitionId.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/PartitionablePartitionId.java @@ -22,9 +22,11 @@ package ca.uhn.fhir.jpa.model.entity; import ca.uhn.fhir.interceptor.model.RequestPartitionId; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.persistence.Column; import javax.persistence.Embeddable; +import javax.validation.constraints.Null; import java.time.LocalDate; @Embeddable @@ -83,4 +85,13 @@ public class PartitionablePartitionId implements Cloneable { public RequestPartitionId toPartitionId() { return RequestPartitionId.fromPartitionId(getPartitionId(), getPartitionDate()); } + + @Nullable + public static RequestPartitionId toRequestPartitionId(@Nullable PartitionablePartitionId theRequestPartitionId) { + if (theRequestPartitionId != null) { + return theRequestPartitionId.toPartitionId(); + } else { + return null; + } + } } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryProvenanceEntity.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryProvenanceEntity.java index 566b2adc849..d97b00eb6f9 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryProvenanceEntity.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryProvenanceEntity.java @@ -104,4 +104,5 @@ public class ResourceHistoryProvenanceEntity extends BasePartitionable { return myId; } + } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryTag.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryTag.java index 839d593ff75..0bb79964686 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryTag.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryTag.java @@ -20,64 +20,53 @@ package ca.uhn.fhir.jpa.model.entity; * #L% */ -import ca.uhn.fhir.interceptor.model.RequestPartitionId; - -import javax.persistence.*; +import javax.persistence.Column; +import javax.persistence.Embeddable; +import javax.persistence.Entity; +import javax.persistence.ForeignKey; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; import java.io.Serializable; @Embeddable @Entity -@Table(name = "HFJ_HISTORY_TAG", uniqueConstraints= { - @UniqueConstraint(name="IDX_RESHISTTAG_TAGID", columnNames= {"RES_VER_PID","TAG_ID"}) +@Table(name = "HFJ_HISTORY_TAG", uniqueConstraints = { + @UniqueConstraint(name = "IDX_RESHISTTAG_TAGID", columnNames = {"RES_VER_PID", "TAG_ID"}) }) public class ResourceHistoryTag extends BaseTag implements Serializable { private static final long serialVersionUID = 1L; - + @SequenceGenerator(name = "SEQ_HISTORYTAG_ID", sequenceName = "SEQ_HISTORYTAG_ID") @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_HISTORYTAG_ID") @Id @Column(name = "PID") private Long myId; - + @ManyToOne() - @JoinColumn(name="RES_VER_PID", referencedColumnName="PID", nullable=false, foreignKey=@ForeignKey(name="FK_HISTORYTAG_HISTORY")) + @JoinColumn(name = "RES_VER_PID", referencedColumnName = "PID", nullable = false, foreignKey = @ForeignKey(name = "FK_HISTORYTAG_HISTORY")) private ResourceHistoryTable myResourceHistory; - @Column(name="RES_VER_PID", insertable = false, updatable = false, nullable = false) + @Column(name = "RES_VER_PID", insertable = false, updatable = false, nullable = false) private Long myResourceHistoryPid; - @Column(name = "RES_TYPE", length = ResourceTable.RESTYPE_LEN, nullable=false) + @Column(name = "RES_TYPE", length = ResourceTable.RESTYPE_LEN, nullable = false) private String myResourceType; - @Column(name="RES_ID", nullable=false) + @Column(name = "RES_ID", nullable = false) private Long myResourceId; - public String getResourceType() { - return myResourceType; - } - - - public void setResourceType(String theResourceType) { - myResourceType = theResourceType; - } - - - public Long getResourceId() { - return myResourceId; - } - - - public void setResourceId(Long theResourceId) { - myResourceId = theResourceId; - } - - public ResourceHistoryTag() { } - - public ResourceHistoryTag(ResourceHistoryTable theResourceHistoryTable, TagDefinition theTag, RequestPartitionId theRequestPartitionId) { + + public ResourceHistoryTag(ResourceHistoryTable theResourceHistoryTable, TagDefinition theTag, PartitionablePartitionId theRequestPartitionId) { setTag(theTag); setResource(theResourceHistoryTable); setResourceId(theResourceHistoryTable.getResourceId()); @@ -85,6 +74,22 @@ public class ResourceHistoryTag extends BaseTag implements Serializable { setPartitionId(theRequestPartitionId); } + public String getResourceType() { + return myResourceType; + } + + public void setResourceType(String theResourceType) { + myResourceType = theResourceType; + } + + public Long getResourceId() { + return myResourceId; + } + + public void setResourceId(Long theResourceId) { + myResourceId = theResourceId; + } + public ResourceHistoryTable getResourceHistory() { return myResourceHistory; } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamQuantity.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamQuantity.java index 8c872b05858..da9827158ff 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamQuantity.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamQuantity.java @@ -283,10 +283,20 @@ public class ResourceIndexedSearchParamQuantity extends BaseResourceIndexedSearc return retval; } + public static long calculateHashSystemAndUnits(PartitionSettings thePartitionSettings, PartitionablePartitionId theRequestPartitionId, String theResourceType, String theParamName, String theSystem, String theUnits) { + RequestPartitionId requestPartitionId = PartitionablePartitionId.toRequestPartitionId(theRequestPartitionId); + return calculateHashSystemAndUnits(thePartitionSettings, requestPartitionId, theResourceType, theParamName, theSystem, theUnits); + } + public static long calculateHashSystemAndUnits(PartitionSettings thePartitionSettings, RequestPartitionId theRequestPartitionId, String theResourceType, String theParamName, String theSystem, String theUnits) { return hash(thePartitionSettings, theRequestPartitionId, theResourceType, theParamName, theSystem, theUnits); } + public static long calculateHashUnits(PartitionSettings thePartitionSettings, PartitionablePartitionId theRequestPartitionId, String theResourceType, String theParamName, String theUnits) { + RequestPartitionId requestPartitionId = PartitionablePartitionId.toRequestPartitionId(theRequestPartitionId); + return calculateHashUnits(thePartitionSettings, requestPartitionId, theResourceType, theParamName, theUnits); + } + public static long calculateHashUnits(PartitionSettings thePartitionSettings, RequestPartitionId theRequestPartitionId, String theResourceType, String theParamName, String theUnits) { return hash(thePartitionSettings, theRequestPartitionId, theResourceType, theParamName, theUnits); } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamString.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamString.java index f3c292a6b40..09328595dbb 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamString.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamString.java @@ -270,10 +270,20 @@ public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchP return defaultString(getValueNormalized()).startsWith(normalizedString); } + public static long calculateHashExact(PartitionSettings thePartitionSettings, PartitionablePartitionId theRequestPartitionId, String theResourceType, String theParamName, String theValueExact) { + RequestPartitionId requestPartitionId = PartitionablePartitionId.toRequestPartitionId(theRequestPartitionId); + return calculateHashExact(thePartitionSettings, requestPartitionId, theResourceType, theParamName, theValueExact); + } + public static long calculateHashExact(PartitionSettings thePartitionSettings, RequestPartitionId theRequestPartitionId, String theResourceType, String theParamName, String theValueExact) { return hash(thePartitionSettings, theRequestPartitionId, theResourceType, theParamName, theValueExact); } + public static long calculateHashNormalized(PartitionSettings thePartitionSettings, PartitionablePartitionId theRequestPartitionId, ModelConfig theModelConfig, String theResourceType, String theParamName, String theValueNormalized) { + RequestPartitionId requestPartitionId = PartitionablePartitionId.toRequestPartitionId(theRequestPartitionId); + return calculateHashNormalized(thePartitionSettings, requestPartitionId, theModelConfig, theResourceType, theParamName, theValueNormalized); + } + public static long calculateHashNormalized(PartitionSettings thePartitionSettings, RequestPartitionId theRequestPartitionId, ModelConfig theModelConfig, String theResourceType, String theParamName, String theValueNormalized) { /* * If we're not allowing contained searches, we'll add the first diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamToken.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamToken.java index 831750ca9a0..20544e8903e 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamToken.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamToken.java @@ -286,14 +286,29 @@ public class ResourceIndexedSearchParamToken extends BaseResourceIndexedSearchPa return retVal; } + public static long calculateHashSystem(PartitionSettings thePartitionSettings, PartitionablePartitionId theRequestPartitionId, String theResourceType, String theParamName, String theSystem) { + RequestPartitionId requestPartitionId = PartitionablePartitionId.toRequestPartitionId(theRequestPartitionId); + return calculateHashSystem(thePartitionSettings, requestPartitionId, theResourceType, theParamName, theSystem); + } + public static long calculateHashSystem(PartitionSettings thePartitionSettings, RequestPartitionId theRequestPartitionId, String theResourceType, String theParamName, String theSystem) { return hash(thePartitionSettings, theRequestPartitionId, theResourceType, theParamName, trim(theSystem)); } + public static long calculateHashSystemAndValue(PartitionSettings thePartitionSettings, PartitionablePartitionId theRequestPartitionId, String theResourceType, String theParamName, String theSystem, String theValue) { + RequestPartitionId requestPartitionId = PartitionablePartitionId.toRequestPartitionId(theRequestPartitionId); + return calculateHashSystemAndValue(thePartitionSettings, requestPartitionId, theResourceType, theParamName, theSystem, theValue); + } + public static long calculateHashSystemAndValue(PartitionSettings thePartitionSettings, RequestPartitionId theRequestPartitionId, String theResourceType, String theParamName, String theSystem, String theValue) { return hash(thePartitionSettings, theRequestPartitionId, theResourceType, theParamName, defaultString(trim(theSystem)), trim(theValue)); } + public static long calculateHashValue(PartitionSettings thePartitionSettings, PartitionablePartitionId theRequestPartitionId, String theResourceType, String theParamName, String theValue) { + RequestPartitionId requestPartitionId = PartitionablePartitionId.toRequestPartitionId(theRequestPartitionId); + return calculateHashValue(thePartitionSettings, requestPartitionId, theResourceType, theParamName, theValue); + } + public static long calculateHashValue(PartitionSettings thePartitionSettings, RequestPartitionId theRequestPartitionId, String theResourceType, String theParamName, String theValue) { String value = trim(theValue); return hash(thePartitionSettings, theRequestPartitionId, theResourceType, theParamName, value); diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamUri.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamUri.java index d3b8f8133aa..6fe89fcf93a 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamUri.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamUri.java @@ -209,6 +209,11 @@ public class ResourceIndexedSearchParamUri extends BaseResourceIndexedSearchPara return defaultString(getUri()).equalsIgnoreCase(uri.getValueNotNull()); } + public static long calculateHashUri(PartitionSettings thePartitionSettings, PartitionablePartitionId theRequestPartitionId, String theResourceType, String theParamName, String theUri) { + RequestPartitionId requestPartitionId = PartitionablePartitionId.toRequestPartitionId(theRequestPartitionId); + return calculateHashUri(thePartitionSettings, requestPartitionId, theResourceType, theParamName, theUri); + } + public static long calculateHashUri(PartitionSettings thePartitionSettings, RequestPartitionId theRequestPartitionId, String theResourceType, String theParamName, String theUri) { return hash(thePartitionSettings, theRequestPartitionId, theResourceType, theParamName, theUri); } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTag.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTag.java index 7a7e338280a..13a2f96e113 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTag.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTag.java @@ -20,13 +20,23 @@ package ca.uhn.fhir.jpa.model.entity; * #L% */ -import ca.uhn.fhir.interceptor.model.RequestPartitionId; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; -import javax.persistence.*; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.ForeignKey; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; @Entity @Table(name = "HFJ_RES_TAG", uniqueConstraints = { @@ -52,10 +62,17 @@ public class ResourceTag extends BaseTag { @Column(name = "RES_ID", insertable = false, updatable = false) private Long myResourceId; + /** + * Constructor + */ public ResourceTag() { + super(); } - public ResourceTag(ResourceTable theResourceTable, TagDefinition theTag, RequestPartitionId theRequestPartitionId) { + /** + * Constructor + */ + public ResourceTag(ResourceTable theResourceTable, TagDefinition theTag, PartitionablePartitionId theRequestPartitionId) { setTag(theTag); setResource(theResourceTable); setResourceId(theResourceTable.getId()); diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/SearchParamPresent.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/SearchParamPresent.java index f29b3d9bfc9..4992bc63db5 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/SearchParamPresent.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/SearchParamPresent.java @@ -126,6 +126,11 @@ public class SearchParamPresent extends BasePartitionable implements Serializabl myPartitionSettings = thePartitionSettings; } + public static long calculateHashPresence(PartitionSettings thePartitionSettings, PartitionablePartitionId theRequestPartitionId, String theResourceType, String theParamName, Boolean thePresent) { + RequestPartitionId requestPartitionId = PartitionablePartitionId.toRequestPartitionId(theRequestPartitionId); + return calculateHashPresence(thePartitionSettings, requestPartitionId, theResourceType, theParamName, thePresent); + } + public static long calculateHashPresence(PartitionSettings thePartitionSettings, RequestPartitionId theRequestPartitionId, String theResourceType, String theParamName, Boolean thePresent) { String string = thePresent != null ? Boolean.toString(thePresent) : Boolean.toString(false); return BaseResourceIndexedSearchParam.hash(thePartitionSettings, theRequestPartitionId, theResourceType, theParamName, string); diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/JpaConstants.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/JpaConstants.java index eacc62ca757..c50f05bb5a0 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/JpaConstants.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/JpaConstants.java @@ -204,6 +204,11 @@ public class JpaConstants { public static final String EXT_SEARCHPARAM_PHONETIC_ENCODER = "http://hapifhir.io/fhir/StructureDefinition/searchparameter-phonetic-encoder"; public static final String VALUESET_FILTER_DISPLAY = "display"; + /** + * The name of the default partition + */ + public static final String DEFAULT_PARTITION_NAME = "DEFAULT"; + /** * Non-instantiable */ diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java index 40b00d0bd54..6e68bc38fb0 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java @@ -215,7 +215,8 @@ public class RuleBuilder implements IAuthRuleBuilder { if (theResource != null) { RequestPartitionId partitionId = (RequestPartitionId) theResource.getUserData(Constants.RESOURCE_PARTITION_ID); if (partitionId != null) { - if (!myTenantIds.contains(partitionId.getPartitionName())) { + String partitionNameOrNull = partitionId.getFirstPartitionNameOrNull(); + if (partitionNameOrNull == null || !myTenantIds.contains(partitionNameOrNull)) { return !myOutcome; } }