diff --git a/src/e2e/java/org/cbioportal/ColumnStoreMutationControllerE2ETest.java b/src/e2e/java/org/cbioportal/ColumnStoreMutationControllerE2ETest.java new file mode 100644 index 00000000000..e5eea49b09b --- /dev/null +++ b/src/e2e/java/org/cbioportal/ColumnStoreMutationControllerE2ETest.java @@ -0,0 +1,132 @@ +package org.cbioportal; + + +import org.cbioportal.application.rest.response.MutationDTO; +import org.cbioportal.shared.enums.ProjectionType; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.HttpMethod; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(SpringExtension.class) +public class ColumnStoreMutationControllerE2ETest extends AbstractE2ETest{ + + @Autowired + private TestRestTemplate restTemplate; + + @LocalServerPort + private int port; + + private static final com.fasterxml.jackson.databind.ObjectMapper OBJECT_MAPPER = new com.fasterxml.jackson.databind.ObjectMapper(); + + + private MutationDTO[] callFetchMutationEndPoint(String testDataJson, ProjectionType projectionType)throws Exception{ + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity requestEntity = new HttpEntity<>(testDataJson, headers); + + ResponseEntity response = restTemplate.exchange( + "http://localhost:" + port + "/api/column-store/mutations/fetch?projection=" + + projectionType.name(), + HttpMethod.POST, + requestEntity, + String.class + ); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertNotNull(response.getBody()); + + return OBJECT_MAPPER.readValue(response.getBody(), MutationDTO[].class); + + } + + private String loadTestData(String filename) throws Exception { + return new String(java.nio.file.Files.readAllBytes( + java.nio.file.Paths.get("src/e2e/java/org/cbioportal/ColumnStoreMutationControllerE2ETest/" + filename))); + } + + @Test + void testFetchMutationEndPointWithDataJson_IdProjection() throws Exception { + // The json has two molecularProfileId and a sampleId and entrezGeneIds to restrict search + // Two profiles meet this criteria + + String testDataJson = loadTestData("mutation_filter.json"); + MutationDTO[] mutationResultID = callFetchMutationEndPoint(testDataJson, ProjectionType.ID); + + assertNotNull(mutationResultID, "Response should have mutation DTO"); + assertEquals(2, mutationResultID.length, "Two mutations meet the search criteria of the json file"); + + + //Compare the fields that should be equal + //MolecularProfile id should be identical + assertEquals(mutationResultID[0].molecularProfileId(), mutationResultID[1].molecularProfileId(), "molecularProfile IDs should match"); + //study id should be identical; + assertEquals(mutationResultID[0].studyId(), mutationResultID[1].studyId(), "study IDs should match"); + + + } + @Test + void testFetchMutationEndPointWithDataJson_SummaryProjection() throws Exception { + // The json has two molecularProfileId and a sampleId and entrezGeneIds to restrict search + // Two profiles meet this criteria + + String testDataJson = loadTestData("mutation_filter.json"); + MutationDTO[] mutationResultSummary = callFetchMutationEndPoint(testDataJson,ProjectionType.SUMMARY); + + assertNotNull(mutationResultSummary, "Response should have mutation DTO"); + assertEquals(2, mutationResultSummary.length, "SUMMARY projection should not add or remove records"); + + + //Compare the fields that should be equal + //MolecularProfile id should be identical + assertEquals(mutationResultSummary[0].molecularProfileId(), mutationResultSummary[1].molecularProfileId(), "molecularProfile IDs should match"); + + //study id should be identical; + assertEquals(mutationResultSummary[0].studyId(), mutationResultSummary[1].studyId(), "study IDs should match"); + + // Testing different projection expose different fields + // SUMMARY projection should not have gene present or any AlleleSpecificCopyNumber. AlleleSpecificCopyNumber is null for this mutation profile + for (MutationDTO mutationDTO : mutationResultSummary) { + assertNull(mutationDTO.gene(), "Response should not have gene present"); + assertNull(mutationDTO.alleleSpecificCopyNumber(), "Response should not have AlleleSpecificCopyNumber present"); + } + + } + + @Test + void testFetchMutationEndPointWithDataJson_DetailedProjection() throws Exception { + // The json has two molecularProfileId and a sampleId and entrezGeneIds to restrict search + // Two profiles meet this criteria + + String testDataJson = loadTestData("mutation_filter.json"); + MutationDTO[] mutationResultDetailed = callFetchMutationEndPoint(testDataJson,ProjectionType.DETAILED); + + assertNotNull(mutationResultDetailed, "Response should have mutation DTO"); + assertEquals(2, mutationResultDetailed.length, "DETAILED projection should not add or remove records"); + + + //Compare the fields that should be equal + + //MolecularProfile id should be identical + assertEquals(mutationResultDetailed[0].molecularProfileId(), mutationResultDetailed[1].molecularProfileId(), "molecularProfile IDs should match"); + //study id should be identical; + assertEquals(mutationResultDetailed[0].studyId(), mutationResultDetailed[1].studyId(), "study IDs should match"); + + // Testing different projection expose different fields + // Detailed projection should have gene present or any AlleleSpecificCopyNumber. AlleleSpecificCopyNumber is null for this mutation profile + for (MutationDTO mutationDTO : mutationResultDetailed) { + assertNotNull(mutationDTO.gene(), "Response should have gene present"); + assertNull(mutationDTO.alleleSpecificCopyNumber(), "Response should not have AlleleSpecificCopyNumber present "); + } + } +} diff --git a/src/e2e/java/org/cbioportal/ColumnStoreMutationControllerE2ETest/mutation_filter.JSON b/src/e2e/java/org/cbioportal/ColumnStoreMutationControllerE2ETest/mutation_filter.JSON new file mode 100644 index 00000000000..6b7b2a2d16c --- /dev/null +++ b/src/e2e/java/org/cbioportal/ColumnStoreMutationControllerE2ETest/mutation_filter.JSON @@ -0,0 +1,7 @@ + { + "sampleMolecularIdentifiers": [ + {"sampleId": "P01_Pri", "molecularProfileId": "lgg_ucsf_2014_mutations"}, + {"sampleId": "P05_Rec", "molecularProfileId": "lgg_ucsf_2014_mutations"} + ], + "entrezGeneIds": [287,2] + } \ No newline at end of file diff --git a/src/main/java/org/cbioportal/application/rest/mapper/AlleleSpecificCopyNumberMapper.java b/src/main/java/org/cbioportal/application/rest/mapper/AlleleSpecificCopyNumberMapper.java new file mode 100644 index 00000000000..db6764cbf77 --- /dev/null +++ b/src/main/java/org/cbioportal/application/rest/mapper/AlleleSpecificCopyNumberMapper.java @@ -0,0 +1,15 @@ +package org.cbioportal.application.rest.mapper; +import org.cbioportal.application.rest.response.AlleleSpecificCopyNumberDTO; +import org.cbioportal.legacy.model.AlleleSpecificCopyNumber; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface AlleleSpecificCopyNumberMapper { + AlleleSpecificCopyNumberMapper INSTANCE = + Mappers.getMapper(AlleleSpecificCopyNumberMapper.class); + + AlleleSpecificCopyNumberDTO toAlleleSpecificCopyNumberDTO( + AlleleSpecificCopyNumber alleleSpecificCopyNumber + ); +} diff --git a/src/main/java/org/cbioportal/application/rest/mapper/GeneMapper.java b/src/main/java/org/cbioportal/application/rest/mapper/GeneMapper.java new file mode 100644 index 00000000000..2e8f0b1cdac --- /dev/null +++ b/src/main/java/org/cbioportal/application/rest/mapper/GeneMapper.java @@ -0,0 +1,13 @@ +package org.cbioportal.application.rest.mapper; + +import org.cbioportal.application.rest.response.GeneDTO; +import org.cbioportal.legacy.model.Gene; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface GeneMapper { + GeneMapper INSTANCE = Mappers.getMapper(GeneMapper.class); + + GeneDTO toGeneDTO(Gene gene); +} diff --git a/src/main/java/org/cbioportal/application/rest/mapper/MutationMapper.java b/src/main/java/org/cbioportal/application/rest/mapper/MutationMapper.java new file mode 100644 index 00000000000..bee68ecd9c1 --- /dev/null +++ b/src/main/java/org/cbioportal/application/rest/mapper/MutationMapper.java @@ -0,0 +1,32 @@ +package org.cbioportal.application.rest.mapper; + +import org.cbioportal.application.rest.response.MutationDTO; + +import org.cbioportal.legacy.model.Mutation; +import org.cbioportal.legacy.utils.Encoder; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper( + imports = Encoder.class, + uses = { GeneMapper.class, AlleleSpecificCopyNumberMapper.class } +) +public interface MutationMapper { + MutationMapper INSTANCE = Mappers.getMapper(MutationMapper.class); + + @Mapping( + target = "uniqueSampleKey", + expression = + "java( Encoder.calculateBase64(mutation.getSampleId()," + "mutation.getStudyId()) )") + @Mapping( + target = "uniquePatientKey", + expression = + "java( Encoder.calculateBase64(mutation.getPatientId(), " + "mutation.getStudyId()) )") + @Mapping(source = "tumorSeqAllele", target = "variantAllele") + MutationDTO toMutationDTOO(Mutation mutation); + + List toDTOs(List mutationList); +} diff --git a/src/main/java/org/cbioportal/application/rest/response/AlleleSpecificCopyNumberDTO.java b/src/main/java/org/cbioportal/application/rest/response/AlleleSpecificCopyNumberDTO.java new file mode 100644 index 00000000000..87085783cd7 --- /dev/null +++ b/src/main/java/org/cbioportal/application/rest/response/AlleleSpecificCopyNumberDTO.java @@ -0,0 +1,13 @@ +package org.cbioportal.application.rest.response; + +public record AlleleSpecificCopyNumberDTO( + Integer ascnIntegerCopyNumber, + String ascnMethod, + Float ccfExpectedCopiesUpper, + Float ccfExpectedCopies, + String clonal, + Integer minorCopyNumber, + Integer expectedAltCopies, + Integer totalCopyNumber +) { +} diff --git a/src/main/java/org/cbioportal/application/rest/response/GeneDTO.java b/src/main/java/org/cbioportal/application/rest/response/GeneDTO.java new file mode 100644 index 00000000000..81ca1ca00d1 --- /dev/null +++ b/src/main/java/org/cbioportal/application/rest/response/GeneDTO.java @@ -0,0 +1,8 @@ +package org.cbioportal.application.rest.response; + +public record GeneDTO( + Integer entrezGeneId, + String hugoGeneSymbol, + String type + ) { +} diff --git a/src/main/java/org/cbioportal/application/rest/response/MutationDTO.java b/src/main/java/org/cbioportal/application/rest/response/MutationDTO.java new file mode 100644 index 00000000000..a701bdf822c --- /dev/null +++ b/src/main/java/org/cbioportal/application/rest/response/MutationDTO.java @@ -0,0 +1,40 @@ +package org.cbioportal.application.rest.response; + + +public record MutationDTO( + String uniqueSampleKey, + String uniquePatientKey, + String molecularProfileId, + String sampleId, + String patientId, + Integer entrezGeneId, + GeneDTO gene, + String studyId, + String driverFilter, + String driverFilterAnnotation, + String driverTiersFilter, + String driverTiersFilterAnnotation, + String center, + String mutationStatus, + String validationStatus, + Integer tumorAltCount, + Integer tumorRefCount, + Integer normalAltCount, + Integer normalRefCount, + String aminoAcidChange, + String chr, + Long startPosition, + Long endPosition, + String referenceAllele, + String variantAllele, + String proteinChange, + String mutationType, + String ncbiBuild, + String variantType, + String refseqMrnaId, + Integer proteinPosStart, + Integer proteinPosEnd, + String keyword, + AlleleSpecificCopyNumberDTO alleleSpecificCopyNumber +) { +} diff --git a/src/main/java/org/cbioportal/application/rest/vcolumnstore/ColumnStoreMutationController.java b/src/main/java/org/cbioportal/application/rest/vcolumnstore/ColumnStoreMutationController.java new file mode 100644 index 00000000000..e0435302032 --- /dev/null +++ b/src/main/java/org/cbioportal/application/rest/vcolumnstore/ColumnStoreMutationController.java @@ -0,0 +1,143 @@ +package org.cbioportal.application.rest.vcolumnstore; + +import io.swagger.v3.oas.annotations.Hidden; +import io.swagger.v3.oas.annotations.Parameter; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import org.cbioportal.application.rest.mapper.MutationMapper; +import org.cbioportal.application.rest.response.MutationDTO; +import org.cbioportal.domain.mutation.usecase.GetMutationUseCases; +import org.cbioportal.legacy.model.meta.MutationMeta; +import org.cbioportal.legacy.web.parameter.*; +import org.cbioportal.legacy.web.parameter.sort.MutationSortBy; +import org.cbioportal.shared.MutationSearchCriteria; +import org.cbioportal.shared.enums.ProjectionType; +import org.springframework.context.annotation.Profile; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.Collection; +import java.util.List; + + +/** + * REST controller for managing and retrieving Mutation data from a column-store data + * source. + * + *

This controller provides an endpoint to fetch Mutation data with support for + * filtering, sorting, and controlling the level of detail in the response. It is designed to work + * with a column-store database, which is optimized for querying large datasets efficiently. + *

+ * + *

Key features: + * + *

    + *
  • Configurable projection levels (ID, SUMMARY, DETAILED) for response optimization + *
  • Multi-study filtering capabilities + *
  • Metadata queries for count operations + *
+ * + *

This controller is only active when the "clickhouse" profile is enabled and requires + * appropriate read permissions for the requested cancer studies. + * + * @see MutationDTO + */ + + +@RestController +@RequestMapping("/api/column-store") +@Profile("clickhouse") +public class ColumnStoreMutationController { + private final GetMutationUseCases getMutationUseCases; + + /** + * Constructs a new {@link ColumnStoreMutationController} with the specified use case. + * + * @param getMutationUseCases the use case responsible for retrieving Mutation metadata or Mutation + * + */ + public ColumnStoreMutationController(GetMutationUseCases getMutationUseCases) { + this.getMutationUseCases = getMutationUseCases; + } + + + /** + * Fetch Mutation by exactly one sampleUniqueIdentifier or molecularProfileId must or entrezGeneIds + * + * + * @param interceptedMutationMultipleStudyFilter security-intercepted filter for permission + * validation + * @param mutationMultipleStudyFilter filter containing patient/sample identifiers and attribute + * IDs + * @param projection level of detail for the response data + * @return ResponseEntity containing list of Mutation data DTOs, or empty body with count header + * for META projection + */ + @Hidden + @PreAuthorize( + "hasPermission(#involvedCancerStudies, 'Collection', T(org.cbioportal.legacy.utils.security.AccessLevel).READ)") + @PostMapping( + value = "/mutations/fetch", + consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> fetchMutationsInMultipleMolecularProfiles( + @Parameter(hidden = true) // prevent reference to this attribute in the swagger-ui interface + @RequestAttribute(required = false, value = "involvedCancerStudies") + Collection involvedCancerStudies, + @Parameter( + hidden = + true) // prevent reference to this attribute in the swagger-ui interface. this + // attribute is needed for now but was needed previously for @PreAuthorize . + @Valid + @RequestBody(required = false) + MutationMultipleStudyFilter interceptedMutationMultipleStudyFilter, // This is being intercepted will leave this + @Parameter( + required = true, + description = + "List of Molecular Profile IDs or List of Molecular Profile ID / Sample ID pairs," + + " and List of Entrez Gene IDs") + @Valid + @RequestBody(required = false) + MutationMultipleStudyFilter mutationMultipleStudyFilter, + @Parameter(description = "Level of detail of the response") + @RequestParam(defaultValue = "SUMMARY") + ProjectionType projection, + @Parameter(description = "Page size of the result list") + @Max(PagingConstants.MAX_PAGE_SIZE) + @Min(PagingConstants.MIN_PAGE_SIZE) + @RequestParam(defaultValue = PagingConstants.DEFAULT_PAGE_SIZE) + Integer pageSize, + @Parameter(description = "Page number of the result list") + @Min(PagingConstants.MIN_PAGE_NUMBER) + @RequestParam(defaultValue = PagingConstants.DEFAULT_PAGE_NUMBER) + Integer pageNumber, + @Parameter(description = "Name of the property that the result list is sorted by") + @RequestParam(required = false) + MutationSortBy sortBy, + @Parameter(description = "Direction of the sort") @RequestParam(defaultValue = "ASC") + Direction direction) { + + if (projection == ProjectionType.META) { + HttpHeaders responseHeaders = new HttpHeaders(); + MutationMeta mutationMeta = getMutationUseCases.fetchMetaMutationsUseCase().execute(interceptedMutationMultipleStudyFilter); + responseHeaders.add(HeaderKeyConstants.TOTAL_COUNT, mutationMeta.getTotalCount().toString()); + responseHeaders.add( + HeaderKeyConstants.SAMPLE_COUNT, mutationMeta.getSampleCount().toString()); + return new ResponseEntity<>(responseHeaders, HttpStatus.OK); + } + MutationSearchCriteria mutationSearchCriteria = new MutationSearchCriteria(projection,pageSize, + pageNumber, + sortBy == null ? null : sortBy.getOriginalValue(), + direction); + List mutations= MutationMapper.INSTANCE.toDTOs(getMutationUseCases.fetchAllMutationsInProfileUseCase() + .execute( + interceptedMutationMultipleStudyFilter, + mutationSearchCriteria)); + return ResponseEntity.ok(mutations); + } +} diff --git a/src/main/java/org/cbioportal/domain/mutation/repository/MutationRepository.java b/src/main/java/org/cbioportal/domain/mutation/repository/MutationRepository.java new file mode 100644 index 00000000000..3907e214d30 --- /dev/null +++ b/src/main/java/org/cbioportal/domain/mutation/repository/MutationRepository.java @@ -0,0 +1,47 @@ +package org.cbioportal.domain.mutation.repository; + + +import org.cbioportal.legacy.model.Mutation; +import org.cbioportal.legacy.model.meta.MutationMeta; +import org.cbioportal.shared.MutationSearchCriteria; + +import java.util.List; + +/** + * Repository interface for accessing mutation data or mutation. + * + *

This abstraction defines the contract for retrieving both detailed mutation records + * and aggregated mutation metadata across multiple molecular profiles, samples, and genes. + */ +public interface MutationRepository { + + /** + * Retrieves a list of mutations that match the specified filters and search criteria. + * + * @param molecularProfileIds List of molecularProfileIds + * @param sampleIds List of sampleIds + * @param entrezGeneIds List of entrezGeneIds + * @param mutationSearchCriteria A criteria to control the appearance of the dataset + * @return a list of {@link Mutation} objects that match the given criteria + * @see MutationSearchCriteria + */ + List getMutationsInMultipleMolecularProfiles( + List molecularProfileIds, + List sampleIds, + List entrezGeneIds, + MutationSearchCriteria mutationSearchCriteria); + + /** + * Retrieves aggregated metadata about mutations that match the specified filters. + * + *

This method is typically used to determine dataset size, counts information without + * fetching full mutation details. + * + * @param molecularProfileIds List of molecularProfileIds + * @param sampleIds List of sampleIds + * @param entrezGeneIds List of Entrez gene identifiers + * @return {@link MutationMeta} containing aggregated information about the dataset + */ + MutationMeta getMetaMutationsInMultipleMolecularProfiles( + List molecularProfileIds, List sampleIds, List entrezGeneIds); +} diff --git a/src/main/java/org/cbioportal/domain/mutation/usecase/FetchAllMutationsInProfileUseCase.java b/src/main/java/org/cbioportal/domain/mutation/usecase/FetchAllMutationsInProfileUseCase.java new file mode 100644 index 00000000000..9b389405f56 --- /dev/null +++ b/src/main/java/org/cbioportal/domain/mutation/usecase/FetchAllMutationsInProfileUseCase.java @@ -0,0 +1,82 @@ +package org.cbioportal.domain.mutation.usecase; + +import org.cbioportal.domain.mutation.repository.MutationRepository; +import org.cbioportal.domain.mutation.util.MutationUtil; + +import org.cbioportal.legacy.model.Mutation; +import org.cbioportal.legacy.web.parameter.MutationMultipleStudyFilter; +import org.cbioportal.shared.MutationSearchCriteria; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +/** + * Use case for retrieving List of Mutation data + * + *

This use case encapsulates the business logic for fetching mutation data based on the provided + * MutationSearchCriteria. it routes requests to the appropriate repository method + *

Supported projection types: + * + *

    + *
  • ID - Returns minimal data with only identifiers + *
  • SUMMARY - Returns basic data + *
  • DETAILED - Returns complete data + *
+ * + * @see MutationRepository + */ + +@Service +@Profile("clickhouse") +public class FetchAllMutationsInProfileUseCase { + private final MutationRepository mutationRepository; + + public FetchAllMutationsInProfileUseCase(MutationRepository mutationRepository) { + this.mutationRepository = mutationRepository; + } + + /** + * Executes the use case to retrieve mutation data based on filter criteria and projection type. + * + *

This method transforms the provided filter into a format suitable for the repository layer. + * If {@code molecularProfileIds} are directly available in the filter, those are used. + * Otherwise, molecular profile IDs and sample IDs are extracted from the filter’s sample–molecular identifiers. + * + *

The {@link MutationSearchCriteria} controls how much data is returned and in what form: + *

    + *
  • projection – level of detail for each mutation (ID, SUMMARY, or DETAILED)
  • + *
  • pageSize – define pagination for the results
  • + *
  • sortBy – field name to sort the results by
  • + *
  • direction – sort order (ASC or DESC)
  • + *
+ * + * @param mutationMultipleStudyFilter filter containing profile, sample, and gene identifiers + * @param mutationSearchCriteria criteria for controlling projection, pagination, and sorting of results + * @return list of {@link Mutation} objects matching the given filter and search criteria + * + * @see MutationSearchCriteria + * @see MutationMultipleStudyFilter + */ + public List execute(MutationMultipleStudyFilter mutationMultipleStudyFilter, + MutationSearchCriteria mutationSearchCriteria) { + if(mutationMultipleStudyFilter.getMolecularProfileIds() != null){ + return mutationRepository.getMutationsInMultipleMolecularProfiles( + mutationMultipleStudyFilter.getMolecularProfileIds(), + null, + mutationMultipleStudyFilter.getEntrezGeneIds(), + mutationSearchCriteria); + } + + List molecularProfileIds= + MutationUtil.extractMolecularProfileIds(mutationMultipleStudyFilter.getSampleMolecularIdentifiers()); + List sampleIds= + MutationUtil.extractSampleIds(mutationMultipleStudyFilter.getSampleMolecularIdentifiers()); + return mutationRepository.getMutationsInMultipleMolecularProfiles( + molecularProfileIds, + sampleIds, + mutationMultipleStudyFilter.getEntrezGeneIds(), + mutationSearchCriteria); + } +} diff --git a/src/main/java/org/cbioportal/domain/mutation/usecase/FetchMetaMutationsUseCase.java b/src/main/java/org/cbioportal/domain/mutation/usecase/FetchMetaMutationsUseCase.java new file mode 100644 index 00000000000..61b95e9c614 --- /dev/null +++ b/src/main/java/org/cbioportal/domain/mutation/usecase/FetchMetaMutationsUseCase.java @@ -0,0 +1,57 @@ +package org.cbioportal.domain.mutation.usecase; + +import org.cbioportal.domain.mutation.repository.MutationRepository; +import org.cbioportal.domain.mutation.util.MutationUtil; +import org.cbioportal.legacy.model.meta.MutationMeta; +import org.cbioportal.legacy.web.parameter.MutationMultipleStudyFilter; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@Profile("clickhouse") + +/** + * Use case for retrieving aggregated mutation metadata. + * + *

This use case determines the correct input parameters from the provided + * {@link MutationMultipleStudyFilter} and delegates the query to the {@link MutationRepository}. + * + *

If molecularProfileIds are available directly in the filter, they are passed + * straight through to the repository. Otherwise, molecular profile IDs and sample IDs are + * extracted from the filter’s sample–molecular identifiers. + */ +public class FetchMetaMutationsUseCase { + private final MutationRepository mutationRepository; + + + public FetchMetaMutationsUseCase(MutationRepository mutationRepository) { + this.mutationRepository = mutationRepository; + } + + /** + * Executes the use case to retrieve metadata about mutations based on the provided filter. + * + * @param mutationMultipleStudyFilter filter containing study, molecular profile, sample, and gene identifiers + * @return aggregated mutation metadata ({@link MutationMeta}) for the given filter + */ + public MutationMeta execute(MutationMultipleStudyFilter mutationMultipleStudyFilter){ + if(mutationMultipleStudyFilter.getMolecularProfileIds() != null){ + return mutationRepository.getMetaMutationsInMultipleMolecularProfiles( + mutationMultipleStudyFilter.getMolecularProfileIds(), + null, + mutationMultipleStudyFilter.getEntrezGeneIds()); + } + List molecularProfileIds = + MutationUtil.extractMolecularProfileIds( + mutationMultipleStudyFilter.getSampleMolecularIdentifiers()); + List sampleIds = + MutationUtil.extractSampleIds( + mutationMultipleStudyFilter.getSampleMolecularIdentifiers()); + return mutationRepository.getMetaMutationsInMultipleMolecularProfiles( + molecularProfileIds, + sampleIds, + mutationMultipleStudyFilter.getEntrezGeneIds()); + } +} diff --git a/src/main/java/org/cbioportal/domain/mutation/usecase/GetMutationUseCases.java b/src/main/java/org/cbioportal/domain/mutation/usecase/GetMutationUseCases.java new file mode 100644 index 00000000000..fa134c3cd05 --- /dev/null +++ b/src/main/java/org/cbioportal/domain/mutation/usecase/GetMutationUseCases.java @@ -0,0 +1,19 @@ +package org.cbioportal.domain.mutation.usecase; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +@Service +@Profile("clickhouse") +/** + * A record representing a collection of use cases related to Mutation data operations. This record + * encapsulates instances of various use case classes, providing a centralized way to access and utilize + * the use cases + * @param fetchAllMetaMutationsInProfileUseCase + * @param fetchAllMutationsInProfileUseCase + */ +public record GetMutationUseCases( + FetchMetaMutationsUseCase fetchMetaMutationsUseCase, + FetchAllMutationsInProfileUseCase fetchAllMutationsInProfileUseCase + ) { +} diff --git a/src/main/java/org/cbioportal/domain/mutation/util/MutationUtil.java b/src/main/java/org/cbioportal/domain/mutation/util/MutationUtil.java new file mode 100644 index 00000000000..ca15e0ba5d7 --- /dev/null +++ b/src/main/java/org/cbioportal/domain/mutation/util/MutationUtil.java @@ -0,0 +1,37 @@ +package org.cbioportal.domain.mutation.util; + +import org.cbioportal.legacy.web.parameter.SampleMolecularIdentifier; + +import java.util.ArrayList; +import java.util.List; + +public abstract class MutationUtil { + private MutationUtil(){} + + /** + * Extracts molecular profile IDs from a list of SampleMolecularIdentifier objects. + * + * @param identifiers the list of SampleMolecularIdentifier Object + * @return a list of molecular profile ID strings + */ + public static List extractMolecularProfileIds(List identifiers) { + List molecularProfileIds= new ArrayList<>(); + identifiers.forEach( + molecularProfileId->molecularProfileIds.add(molecularProfileId.getMolecularProfileId())); + return molecularProfileIds; + } + + /** + * Extracts molecular profile IDs from a list of SampleMolecularIdentifier objects. + * + * @param identifiers the list of SampleMolecularIdentifier objects + * @return a list of sample ID strings + */ + public static List extractSampleIds(List identifiers) { + List sampleIds= new ArrayList<>(); + identifiers.forEach( + sampleId->sampleIds.add(sampleId.getSampleId())); + return sampleIds; + } + +} diff --git a/src/main/java/org/cbioportal/infrastructure/repository/clickhouse/mutation/ClickhouseMutationMapper.java b/src/main/java/org/cbioportal/infrastructure/repository/clickhouse/mutation/ClickhouseMutationMapper.java new file mode 100644 index 00000000000..d4a6eb8b738 --- /dev/null +++ b/src/main/java/org/cbioportal/infrastructure/repository/clickhouse/mutation/ClickhouseMutationMapper.java @@ -0,0 +1,113 @@ +package org.cbioportal.infrastructure.repository.clickhouse.mutation; + +import org.cbioportal.legacy.model.Mutation; +import org.cbioportal.legacy.model.meta.MutationMeta; + +import java.util.List; + + + +/** + * Mapper interface for retrieving Mutation data from ClickHouse. This interface provides methods to + * fetch Mutation counts and Mutation data for molecular profile,samples and entrez Gene Ids. + */ +public interface ClickhouseMutationMapper { + + + /** + * Retrieves mutation with ID projection (minimal data set). + * + *

Returns only essential identifiers: molecularProfileId, sampleId, patientId, entrezGeneId and + * studyId. + * + * @param allMolecularProfileIds list of distinct molecularProfile + * @param allSampleIds list of distinct sampleIds + * @param entrezGeneIds list of entrez gene identifiers to filter by + * @param snpOnly snpOnly flag indicating whether to restrict results to single nucleotide polymorphisms (SNPs) only + * @param projection level of detail for each mutation to return for each mutation (e.g. ID, SUMMARY, DETAILED) + * @param limit limit maximum number of results to return (for pagination) + * @param offset offset number of results to skip before returning (for pagination) + * @return list of mutation matching the criteria + */ + List getMutationsInMultipleMolecularProfilesId( + List allMolecularProfileIds, + List allSampleIds, + List entrezGeneIds, + boolean snpOnly, // Currently hardcoded to false due to how the legacy worked + String projection, + Integer limit, + Integer offset); + + /** + * Retrieves mutation with SUMMARY projection (basic data with values). + * + *

Returns basic mutation information, but without detailed mutation metadata. + * + * @param allMolecularProfileIds list of distinct molecularProfile + * @param allSampleIds list of distinct sampleIds + * @param entrezGeneIds list of entrez gene identifiers to filter by + * @param snpOnly snpOnly flag indicating whether to restrict results to single nucleotide polymorphisms (SNPs) only + * @param projection level of detail for each mutation to return for each mutation (e.g. ID, SUMMARY, DETAILED) + * @param limit limit maximum number of results to return (for pagination) + * @param offset offset number of results to skip before returning (for pagination) + * @param sortBy sortBy field name to sort results by + * @param direction sort direction, typically "ASC" or "DESC" + * @return list of mutation matching the criteria + */ + List getSummaryMutationsInMultipleMolecularProfiles( + List allMolecularProfileIds, + List allSampleIds, + List entrezGeneIds, + boolean snpOnly, // Currently hardcoded to false due to how the legacy worked + String projection, + Integer limit, + Integer offset, + String sortBy, + String direction); + + /** + * Retrieves mutation with DETAILED projection (complete data set) + * + *

Returns complete mutation data including all mutation fields. This projection provides + * the most comprehensive data but may have higher performance costs due to joins. + * + * @param allMolecularProfileIds list of distinct molecularProfile + * @param allSampleIds list of distinct sampleIds + * @param entrezGeneIds list of entrez gene identifiers to filter by + * @param snpOnly snpOnly flag indicating whether to restrict results to single nucleotide polymorphisms (SNPs) only + * @param projection level of detail for each mutation to return for each mutation (e.g. ID, SUMMARY, DETAILED) + * @param limit limit maximum number of results to return (for pagination) + * @param offset offset number of results to skip before returning (for pagination) + * @param sortBy sortBy field name to sort results by + * @param direction sort direction, typically "ASC" or "DESC" + * @return list of mutation matching the criteria + */ + List getDetailedMutationsInMultipleMolecularProfiles( + List allMolecularProfileIds, + List allSampleIds, + List entrezGeneIds, + boolean snpOnly, // Currently hardcoded to false due to how the legacy worked + String projection, + Integer limit, + Integer offset, + String sortBy, + String direction); + + /** + * Retrieves the count of mutation matching the specified criteria. + * + *

Returns total count and sample count that would be returned by a corresponding data + * retrieval operation, without actually fetching the data. + * @param allMolecularProfileIds list of distinct molecularProfile + * @param allSampleIds list of distinct sampleIds + * @param entrezGeneIds list of entrez gene identifiers to filter by + * @param snpOnly snpOnly flag indicating whether to restrict results to single nucleotide polymorphisms (SNPs) only + * @return MutationMeta + */ + + MutationMeta getMetaMutationsInMultipleMolecularProfiles( List allMolecularProfileIds, + List allSampleIds, + List entrezGeneIds, + boolean snpOnly // Currently hardcoded to false due to how the legacy worked + ); +} diff --git a/src/main/java/org/cbioportal/infrastructure/repository/clickhouse/mutation/ClickhouseMutationRepository.java b/src/main/java/org/cbioportal/infrastructure/repository/clickhouse/mutation/ClickhouseMutationRepository.java new file mode 100644 index 00000000000..5d2ce8d5e67 --- /dev/null +++ b/src/main/java/org/cbioportal/infrastructure/repository/clickhouse/mutation/ClickhouseMutationRepository.java @@ -0,0 +1,92 @@ +package org.cbioportal.infrastructure.repository.clickhouse.mutation; + +import org.cbioportal.domain.mutation.repository.MutationRepository; +import org.cbioportal.legacy.model.Mutation; +import org.cbioportal.legacy.model.meta.MutationMeta; +import org.cbioportal.legacy.persistence.mybatis.util.MolecularProfileCaseIdentifierUtil; +import org.cbioportal.legacy.persistence.mybatis.util.PaginationCalculator; +import org.cbioportal.shared.MutationSearchCriteria; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Repository; + +import java.util.*; +@Repository +@Profile("clickhouse") +public class ClickhouseMutationRepository implements MutationRepository { + + private final ClickhouseMutationMapper mapper; + private final MolecularProfileCaseIdentifierUtil molecularProfileCaseIdentifierUtil; + + + public ClickhouseMutationRepository(ClickhouseMutationMapper clickhouseMutationMapper) { + this.mapper = clickhouseMutationMapper; + this.molecularProfileCaseIdentifierUtil = new MolecularProfileCaseIdentifierUtil(); + } + + @Override + public List getMutationsInMultipleMolecularProfiles( + List molecularProfileIds, + List sampleIds, + List entrezGeneIds, + MutationSearchCriteria mutationSearchCriteria){ + + Integer limit= mutationSearchCriteria.pageSize(); + Integer offset= PaginationCalculator.offset(mutationSearchCriteria.pageSize(),mutationSearchCriteria.pageNumber()); + + Map> groupedCases= molecularProfileCaseIdentifierUtil + .getGroupedCasesByMolecularProfileId(molecularProfileIds,sampleIds); + + List allMolecularProfileIds= new ArrayList<>(groupedCases.keySet()); + List allSampleIds = groupedCases.values().stream().flatMap(Collection::stream).distinct().toList(); + + var projection=mutationSearchCriteria.projection(); + return switch (projection){ + case ID-> + mapper.getMutationsInMultipleMolecularProfilesId( + allMolecularProfileIds, + allSampleIds, + entrezGeneIds, + false, + mutationSearchCriteria.projection().name(), + limit, + offset); + case SUMMARY-> + mapper.getSummaryMutationsInMultipleMolecularProfiles( + allMolecularProfileIds, + allSampleIds, + entrezGeneIds, + false, + mutationSearchCriteria.projection().name(), + limit, + offset, + mutationSearchCriteria.sortBy(), + mutationSearchCriteria.direction().name() + ); + case DETAILED-> + mapper.getDetailedMutationsInMultipleMolecularProfiles( + allMolecularProfileIds, + allSampleIds, + entrezGeneIds, + false, + mutationSearchCriteria.projection().name(), + limit, + offset, + mutationSearchCriteria.sortBy(), + mutationSearchCriteria.direction().name() + ); + default -> new ArrayList<>(); + }; + } + + @Override + public MutationMeta getMetaMutationsInMultipleMolecularProfiles(List molecularProfileIds, + List sampleIds, + List entrezGeneIds) { + Map> groupedCases= molecularProfileCaseIdentifierUtil + .getGroupedCasesByMolecularProfileId(molecularProfileIds,sampleIds); + + List allMolecularProfileIds= new ArrayList<>(groupedCases.keySet()); + List allSampleIds = groupedCases.values().stream().flatMap(Collection::stream).distinct().toList(); + return mapper.getMetaMutationsInMultipleMolecularProfiles(allMolecularProfileIds,allSampleIds,entrezGeneIds,false); + } +} diff --git a/src/main/java/org/cbioportal/shared/MutationSearchCriteria.java b/src/main/java/org/cbioportal/shared/MutationSearchCriteria.java new file mode 100644 index 00000000000..aa8de175d6d --- /dev/null +++ b/src/main/java/org/cbioportal/shared/MutationSearchCriteria.java @@ -0,0 +1,11 @@ +package org.cbioportal.shared; + +import org.cbioportal.legacy.web.parameter.Direction; +import org.cbioportal.shared.enums.ProjectionType; + +public record MutationSearchCriteria(ProjectionType projection, + Integer pageSize, + Integer pageNumber, + String sortBy, + Direction direction ) { +} diff --git a/src/main/resources/mappers/clickhouse/mutation/ClickhouseMutationDataMapper.xml b/src/main/resources/mappers/clickhouse/mutation/ClickhouseMutationDataMapper.xml new file mode 100644 index 00000000000..365a358b74e --- /dev/null +++ b/src/main/resources/mappers/clickhouse/mutation/ClickhouseMutationDataMapper.xml @@ -0,0 +1,217 @@ + + + + + + + WITH mutation_agg AS ( + SELECT + genetic_profile_id, + entrez_gene_id, + any(center) AS center, + any(tumor_alt_count) AS tumor_alt_count, + any(tumor_ref_count) AS tumor_ref_count, + any(normal_alt_count) AS normal_alt_count, + any(normal_ref_count) AS normal_ref_count, + any(amino_acid_change) AS amino_acid_change, + any(annotation_json) AS annotation_json, + any(mutation_event_id) AS mutation_event_id, + any(sample_id) AS sample_id, + any(mutation_status) AS mutation_status, + any(validation_status) AS validation_status + FROM mutation + GROUP BY genetic_profile_id, entrez_gene_id + ) + + +-- Leslie + SELECT + ged.genetic_profile_stable_id AS molecularProfileId, + replaceOne(ged.sample_unique_id, concat(ged.cancer_study_identifier, '_'), '') AS sampleId, + replaceOne(ged.patient_unique_id, concat(ged.cancer_study_identifier, '_'), '') AS patientId, + ged.entrez_gene_id AS entrezGeneId, + ged.cancer_study_identifier AS studyId + + , + mu.center AS "center", + mu.mutation_status AS "mutationStatus", + mu.validation_status AS "validationStatus", + mu.tumor_alt_count AS "tumorAltCount", + mu.tumor_ref_count AS "tumorRefCount", + mu.normal_alt_count AS "normalAltCount", + mu.normal_ref_count AS "normalRefCount", + mu.amino_acid_change AS "aminoAcidChange", + me.chr AS "chr", + me.start_position AS "startPosition", + me.end_position AS "endPosition", + me.reference_allele AS "referenceAllele", + me.tumor_seq_allele AS "tumorSeqAllele", + me.protein_change AS "proteinChange", + ged.mutation_type AS mutationType, + me.ncbi_build AS "ncbiBuild", + me.variant_type AS variantType, + me.refseq_mrna_id AS "refseqMrnaId", + me.protein_pos_start AS "proteinPosStart", + me.protein_pos_end AS "proteinPosEnd", + me.keyword AS "keyword", + mu.annotation_json AS "annotationJSON", + NULLIF(ged.driver_filter, 'NA') AS "driverFilter", + ada.driver_filter_annotation AS "driverFilterAnnotation", + NULLIF(ged.driver_tiers_filter, 'NA') AS "driverTiersFilter", + ada.driver_tiers_filter_annotation AS "driverTiersFilterAnnotation" + + + , + g.entrez_gene_id AS "gene.entrezGeneId", + g.hugo_gene_symbol AS "gene.hugoGeneSymbol", + g.type AS "gene.type" + , + + + + + + + + ascn.ascn_integer_copy_number AS "${prefix}ascnIntegerCopyNumber", + ascn.ascn_method AS "${prefix}ascnMethod", + ascn.ccf_expected_copies_upper AS "${prefix}ccfExpectedCopiesUpper", + ascn.ccf_expected_copies AS "${prefix}ccfExpectedCopies", + ascn.clonal AS "${prefix}clonal", + ascn.minor_copy_number AS "${prefix}minorCopyNumber", + ascn.expected_alt_copies AS "${prefix}expectedAltCopies", + ascn.total_copy_number AS "${prefix}totalCopyNumber" + + + + + + + + ORDER BY "${sortBy}" ${direction} + + + LIMIT #{limit} OFFSET #{offset} + + + + + + ORDER BY genomic_event_derived.genetic_profile_stable_id ASC, + genomic_event_derived.sample_unique_id ASC, + genomic_event_derived.entrez_gene_id ASC + + LIMIT #{limit} OFFSET #{offset} + + + + + + + + + + replaceOne(genomic_event_derived.sample_unique_id, concat(genomic_event_derived.cancer_study_identifier, '_'), '') + IN + + #{sampleId} + + + + + + + AND + + genomic_event_derived.genetic_profile_stable_id IN + + #{profileId} + + + + + + + AND + + genomic_event_derived.entrez_gene_id IN + + #{geneId} + + + -- Extra guard to protect retrieving other types of event from derived table + AND endsWith(genetic_profile_stable_id, '_mutations') + + + + + + FROM genomic_event_derived ged + INNER JOIN genetic_profile gp + ON gp.stable_id = ged.genetic_profile_stable_id + LEFT JOIN mutation_agg mu + ON mu.entrez_gene_id = ged.entrez_gene_id + AND mu.genetic_profile_id = gp.genetic_profile_id + INNER JOIN mutation_event me + ON me.mutation_event_id = mu.mutation_event_id + LEFT JOIN alteration_driver_annotation ada + ON ada.sample_id = mu.sample_id + AND ada.genetic_profile_id = mu.genetic_profile_id + AND ada.alteration_event_id = mu.mutation_event_id + + INNER JOIN gene g + ON g.entrez_gene_id = ged.entrez_gene_id + + + + + + + LEFT JOIN allele_specific_copy_number ascn + ON ascn.sample_id = mu.sample_id + AND ascn.genetic_profile_id = mu.genetic_profile_id + AND ascn.mutation_event_id = mu.mutation_event_id + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/java/org/cbioportal/domain/mutation/usecase/FetchAllMutationsInProfileUseCaseTest.java b/src/test/java/org/cbioportal/domain/mutation/usecase/FetchAllMutationsInProfileUseCaseTest.java new file mode 100644 index 00000000000..6380510dcb0 --- /dev/null +++ b/src/test/java/org/cbioportal/domain/mutation/usecase/FetchAllMutationsInProfileUseCaseTest.java @@ -0,0 +1,111 @@ +package org.cbioportal.domain.mutation.usecase; + +import org.cbioportal.domain.mutation.repository.MutationRepository; +import org.cbioportal.legacy.model.Mutation; +import org.cbioportal.legacy.web.parameter.MutationMultipleStudyFilter; +import org.cbioportal.legacy.web.parameter.Projection; +import org.cbioportal.legacy.web.parameter.SampleMolecularIdentifier; +import org.cbioportal.shared.MutationSearchCriteria; +import org.cbioportal.shared.enums.ProjectionType; +import org.jetbrains.annotations.NotNull; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + + +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + + +@RunWith(MockitoJUnitRunner.class) +public class FetchAllMutationsInProfileUseCaseTest { + @InjectMocks private FetchAllMutationsInProfileUseCase fetchAllMutationsInProfileUseCase; + + @Mock private MutationRepository mutationRepository; + + + private static @NotNull List getSampleMolecularIdentifiers() { + var sampleMolecularIdentifier1= new SampleMolecularIdentifier(); + var sampleMolecularIdentifier2= new SampleMolecularIdentifier(); + sampleMolecularIdentifier1.setSampleId("TCGA-A1-A0SH-01"); + sampleMolecularIdentifier1.setMolecularProfileId("study_tcga_pub_mutations"); + sampleMolecularIdentifier2.setSampleId("TCGA-A1-A0SO-01"); + sampleMolecularIdentifier2.setMolecularProfileId("study_tcga_pub_mutations"); + return List.of(sampleMolecularIdentifier1,sampleMolecularIdentifier2); + } + + @Test + public void testExecuteWithGetMolecularProfileIdsNotNull() { + MutationMultipleStudyFilter mutationMultipleStudyFilter; + MutationSearchCriteria mutationSearchCriteria; + mutationSearchCriteria = new MutationSearchCriteria( + ProjectionType.META, + null, + null, + null, + null + ); + mutationMultipleStudyFilter = new MutationMultipleStudyFilter(); + mutationMultipleStudyFilter.setMolecularProfileIds(List.of("study_tcga_pub_mutations","TCGA-A1-A0SO-01")); + mutationMultipleStudyFilter.setEntrezGeneIds(List.of(672)); + when(mutationRepository.getMutationsInMultipleMolecularProfiles( + anyList(), any(), anyList(), any())).thenReturn(Collections.emptyList()); + + + List result = fetchAllMutationsInProfileUseCase.execute( + mutationMultipleStudyFilter, + mutationSearchCriteria + ); + verify(mutationRepository).getMutationsInMultipleMolecularProfiles( + eq(mutationMultipleStudyFilter.getMolecularProfileIds()), + isNull(), + eq(mutationMultipleStudyFilter.getEntrezGeneIds()), + eq(mutationSearchCriteria) + ); + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + @Test + public void testExecuteWithGetMolecularProfileIdsNull() { + MutationMultipleStudyFilter mutationMultipleStudyFilter; + MutationSearchCriteria mutationSearchCriteria; + var listOfSampleMolecularIdentifiers = getSampleMolecularIdentifiers(); + mutationMultipleStudyFilter = new MutationMultipleStudyFilter(); + mutationMultipleStudyFilter.setSampleMolecularIdentifiers(listOfSampleMolecularIdentifiers); + mutationMultipleStudyFilter.setEntrezGeneIds(List.of(672)); + + mutationSearchCriteria = new MutationSearchCriteria( + ProjectionType.META, + null, + null, + null, + null + ); + when(mutationRepository.getMutationsInMultipleMolecularProfiles( + anyList(), any(), anyList(), any())).thenReturn(Collections.emptyList()); + + + List result = fetchAllMutationsInProfileUseCase.execute( + mutationMultipleStudyFilter, + mutationSearchCriteria + ); + verify(mutationRepository).getMutationsInMultipleMolecularProfiles( + eq(List.of("study_tcga_pub_mutations", "study_tcga_pub_mutations")), + eq(List.of("TCGA-A1-A0SH-01", "TCGA-A1-A0SO-01")), + eq(mutationMultipleStudyFilter.getEntrezGeneIds()), + eq(mutationSearchCriteria) + ); + assertNotNull(result); + assertTrue(result.isEmpty()); + } + +} \ No newline at end of file diff --git a/src/test/java/org/cbioportal/domain/mutation/usecase/FetchMetaMutationsUseCaseTest.java b/src/test/java/org/cbioportal/domain/mutation/usecase/FetchMetaMutationsUseCaseTest.java new file mode 100644 index 00000000000..a095470d4c1 --- /dev/null +++ b/src/test/java/org/cbioportal/domain/mutation/usecase/FetchMetaMutationsUseCaseTest.java @@ -0,0 +1,82 @@ +package org.cbioportal.domain.mutation.usecase; + + +import org.cbioportal.domain.mutation.repository.MutationRepository; +import org.cbioportal.legacy.model.meta.MutationMeta; +import org.cbioportal.legacy.web.parameter.MutationMultipleStudyFilter; +import org.cbioportal.legacy.web.parameter.SampleMolecularIdentifier; +import org.jetbrains.annotations.NotNull; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.List; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class FetchMetaMutationsUseCaseTest { + + @InjectMocks + private FetchMetaMutationsUseCase fetchMetaMutationsUseCase; + @Mock + private MutationRepository mutationRepository; + + private static @NotNull List getSampleMolecularIdentifiers() { + var sampleMolecularIdentifier1= new SampleMolecularIdentifier(); + var sampleMolecularIdentifier2= new SampleMolecularIdentifier(); + sampleMolecularIdentifier1.setSampleId("TCGA-A1-A0SH-01"); + sampleMolecularIdentifier1.setMolecularProfileId("study_tcga_pub_mutations"); + sampleMolecularIdentifier2.setSampleId("TCGA-A1-A0SO-01"); + sampleMolecularIdentifier2.setMolecularProfileId("study_tcga_pub_mutations"); + return List.of(sampleMolecularIdentifier1,sampleMolecularIdentifier2); + } + + @Test + public void testExecuteWithGetMolecularProfileIdsNull() { + MutationMultipleStudyFilter mutationMultipleStudyFilter; + mutationMultipleStudyFilter = new MutationMultipleStudyFilter(); + mutationMultipleStudyFilter.setSampleMolecularIdentifiers(getSampleMolecularIdentifiers()); + mutationMultipleStudyFilter.setEntrezGeneIds(List.of(672)); + + when(mutationRepository.getMetaMutationsInMultipleMolecularProfiles( + anyList(), any(), anyList())) + .thenReturn(new MutationMeta()); + + MutationMeta result = fetchMetaMutationsUseCase.execute(mutationMultipleStudyFilter); + verify(mutationRepository).getMetaMutationsInMultipleMolecularProfiles( + eq(List.of("study_tcga_pub_mutations", "study_tcga_pub_mutations")), + eq(List.of("TCGA-A1-A0SH-01", "TCGA-A1-A0SO-01")), + eq(mutationMultipleStudyFilter.getEntrezGeneIds()) + ); + assertNotNull(result); + } + + @Test + public void testExecuteWithGetMolecularProfileIdsNotNull() { + MutationMultipleStudyFilter mutationMultipleStudyFilter; + mutationMultipleStudyFilter = new MutationMultipleStudyFilter(); + mutationMultipleStudyFilter.setMolecularProfileIds(List.of("study_tcga_pub_mutations","TCGA-A1-A0SO-01")); + mutationMultipleStudyFilter.setEntrezGeneIds(List.of(672)); + + when(mutationRepository.getMetaMutationsInMultipleMolecularProfiles( + anyList(), any(), anyList())) + .thenReturn(new MutationMeta()); + + MutationMeta result = fetchMetaMutationsUseCase.execute(mutationMultipleStudyFilter); + verify(mutationRepository).getMetaMutationsInMultipleMolecularProfiles( + eq(mutationMultipleStudyFilter.getMolecularProfileIds()), + isNull(), + eq(mutationMultipleStudyFilter.getEntrezGeneIds()) + ); + assertNotNull(result); + } +} \ No newline at end of file diff --git a/src/test/java/org/cbioportal/domain/mutation/util/MutationUtilTest.java b/src/test/java/org/cbioportal/domain/mutation/util/MutationUtilTest.java new file mode 100644 index 00000000000..6fa2c14b07c --- /dev/null +++ b/src/test/java/org/cbioportal/domain/mutation/util/MutationUtilTest.java @@ -0,0 +1,46 @@ +package org.cbioportal.domain.mutation.util; + +import org.cbioportal.legacy.web.parameter.MutationMultipleStudyFilter; +import org.cbioportal.legacy.web.parameter.SampleMolecularIdentifier; +import org.jetbrains.annotations.NotNull; +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.*; + +public class MutationUtilTest { + + private List sampleMolecularIdentifierList; + + @Before + public void setUp() throws Exception { + sampleMolecularIdentifierList=new ArrayList<>(); + var sampleMolecularIdentifier1= new SampleMolecularIdentifier(); + var sampleMolecularIdentifier2= new SampleMolecularIdentifier(); + sampleMolecularIdentifier1.setSampleId("TCGA-A1-A0SH-01"); + sampleMolecularIdentifier1.setMolecularProfileId("study_tcga_pub_mutations"); + sampleMolecularIdentifier2.setSampleId("TCGA-A1-A0SO-01"); + sampleMolecularIdentifier2.setMolecularProfileId("study_tcga_pub_mutations"); + sampleMolecularIdentifierList.add(sampleMolecularIdentifier1); + sampleMolecularIdentifierList.add(sampleMolecularIdentifier2); + } + + @Test + public void extractMolecularProfileIds() { + List resultMolecularProfileIds = MutationUtil.extractMolecularProfileIds(sampleMolecularIdentifierList); + assertEquals(2, resultMolecularProfileIds.size()); + assertEquals("study_tcga_pub_mutations", resultMolecularProfileIds.getFirst()); + assertEquals("study_tcga_pub_mutations", resultMolecularProfileIds.get(1)); + } + + @Test + public void extractSampleIds() { + List resultSampleIds = MutationUtil.extractSampleIds(sampleMolecularIdentifierList); + assertEquals(2, resultSampleIds.size()); + assertEquals("TCGA-A1-A0SH-01", resultSampleIds.getFirst()); + assertEquals("TCGA-A1-A0SO-01", resultSampleIds.get(1)); + } +} \ No newline at end of file diff --git a/src/test/java/org/cbioportal/infrastructure/repository/clickhouse/mutation/ClickhouseMutationMapperTest.java b/src/test/java/org/cbioportal/infrastructure/repository/clickhouse/mutation/ClickhouseMutationMapperTest.java new file mode 100644 index 00000000000..b3ab1a968fb --- /dev/null +++ b/src/test/java/org/cbioportal/infrastructure/repository/clickhouse/mutation/ClickhouseMutationMapperTest.java @@ -0,0 +1,98 @@ +package org.cbioportal.infrastructure.repository.clickhouse.mutation; + +import org.cbioportal.infrastructure.repository.clickhouse.AbstractTestcontainers; +import org.cbioportal.infrastructure.repository.clickhouse.config.MyBatisConfig; +import org.cbioportal.legacy.model.Mutation; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.*; + +@RunWith(SpringRunner.class) +@Import(MyBatisConfig.class) +@DataJpaTest +@DirtiesContext +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@ContextConfiguration(initializers = AbstractTestcontainers.Initializer.class) +public class ClickhouseMutationMapperTest { + + @Autowired private ClickhouseMutationMapper clickhouseMutationMapper; + + @Test + public void getMutationsInMultipleMolecularProfilesIdProjection() { + var allMolecularProfileIds = List.of("study_tcga_pub_mutations"); + var allSampleIds = List.of("tcga-a1-a0sh-01"); + var entrezGeneIds = List.of(672); + + + + var result = clickhouseMutationMapper.getMutationsInMultipleMolecularProfilesId(allMolecularProfileIds,allSampleIds, + entrezGeneIds, false, "ID", null, null); + + assertEquals(2, result.size()); + result.forEach(mutation -> { + assertEquals("study_tcga_pub_mutations", mutation.getMolecularProfileId()); + assertEquals("tcga-a1-a0sh-01", mutation.getSampleId()); + assertEquals((Integer) 672, mutation.getEntrezGeneId()); + }); + } + + @Test + public void getMetaMutationsInMultipleMolecularProfiles() { + var allMolecularProfileIds = List.of("study_tcga_pub_mutations"); + var allSampleIds = List.of("tcga-a1-a0sh-01"); + var entrezGeneIds = List.of(672); + + + var result = clickhouseMutationMapper.getMetaMutationsInMultipleMolecularProfiles( + allMolecularProfileIds, allSampleIds, entrezGeneIds, false); + + assertEquals((Integer) 2, result.getTotalCount()); + assertEquals((Integer) 1, result.getSampleCount()); + } + + @Test + public void getMetaMutationsInMultipleMolecularProfiles_SampleIdEmpty() { + var allMolecularProfileIds = List.of("study_tcga_pub_mutations"); + var allSampleIds = new ArrayList(); + var entrezGeneIds = List.of(672); + + + var result = clickhouseMutationMapper.getMetaMutationsInMultipleMolecularProfiles( + allMolecularProfileIds, allSampleIds, entrezGeneIds, false); + + assertEquals((Integer) 5, result.getTotalCount()); + assertEquals((Integer) 4, result.getSampleCount()); + } + + @Test + public void getMetaMutationsInMultipleMolecularProfiles_ProjectionSize() { + var allMolecularProfileIds = List.of("study_tcga_pub_mutations"); + var allSampleIds = new ArrayList(); + var entrezGeneIds = List.of(672); + + //Calling meta,detailed and summary projection + List resultID = clickhouseMutationMapper.getMutationsInMultipleMolecularProfilesId(allMolecularProfileIds,allSampleIds, + entrezGeneIds, false, "ID", null, null); + + List resultDetailed = clickhouseMutationMapper.getSummaryMutationsInMultipleMolecularProfiles(allMolecularProfileIds,allSampleIds, + entrezGeneIds, false, "DETAILED", null, null, null, null); + + List resultSummary = clickhouseMutationMapper.getSummaryMutationsInMultipleMolecularProfiles(allMolecularProfileIds,allSampleIds, + entrezGeneIds, false, "SUMMARY", null, null, null, null); + + //Should produce the same amount of result + assertEquals(5, resultDetailed.size()); + assertEquals(5, resultSummary.size()); + assertEquals(5, resultID.size()); + } +} \ No newline at end of file