diff --git a/.gitignore b/.gitignore index a98205f8c3..e613dc3e0c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +WebAPIConfig/ *application.properties .idea/ .metadata/ diff --git a/pom.xml b/pom.xml index 3f011889c9..4dc14d0b0c 100644 --- a/pom.xml +++ b/pom.xml @@ -205,6 +205,8 @@ info info info + info + info warn 10 @@ -949,6 +951,11 @@ 2.0.1 test + + log4j + log4j + 1.2.17 + diff --git a/src/main/java/org/ohdsi/webapi/cohortsample/CleanupCohortSamplesTasklet.java b/src/main/java/org/ohdsi/webapi/cohortsample/CleanupCohortSamplesTasklet.java new file mode 100644 index 0000000000..269230f39a --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/cohortsample/CleanupCohortSamplesTasklet.java @@ -0,0 +1,145 @@ +package org.ohdsi.webapi.cohortsample; + +import org.ohdsi.webapi.cohortdefinition.CleanupCohortTasklet; +import org.ohdsi.webapi.job.JobExecutionResource; +import org.ohdsi.webapi.job.JobTemplate; +import org.ohdsi.webapi.source.Source; +import org.ohdsi.webapi.source.SourceDaimon; +import org.ohdsi.webapi.source.SourceRepository; +import org.ohdsi.webapi.util.PreparedStatementRenderer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; +import org.springframework.batch.core.configuration.annotation.StepBuilderFactory; +import org.springframework.batch.core.job.builder.SimpleJobBuilder; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.transaction.support.TransactionTemplate; + +import java.util.List; +import java.util.Map; + +import static org.ohdsi.webapi.Constants.Params.COHORT_DEFINITION_ID; +import static org.ohdsi.webapi.Constants.Params.JOB_NAME; +import static org.ohdsi.webapi.Constants.Params.SOURCE_ID; + +public class CleanupCohortSamplesTasklet implements Tasklet { + private static final Logger log = LoggerFactory.getLogger(CleanupCohortTasklet.class); + + private final TransactionTemplate transactionTemplate; + private final SourceRepository sourceRepository; + private final CohortSamplingService samplingService; + private final CohortSampleRepository sampleRepository; + + public CleanupCohortSamplesTasklet( + final TransactionTemplate transactionTemplate, + final SourceRepository sourceRepository, + CohortSamplingService samplingService, + CohortSampleRepository sampleRepository + ) { + this.transactionTemplate = transactionTemplate; + this.sourceRepository = sourceRepository; + this.samplingService = samplingService; + this.sampleRepository = sampleRepository; + } + + private Integer doTask(ChunkContext chunkContext) { + Map jobParams = chunkContext.getStepContext().getJobParameters(); + int cohortDefinitionId = Integer.parseInt(jobParams.get(COHORT_DEFINITION_ID).toString()); + + if (jobParams.containsKey(SOURCE_ID)) { + int sourceId = Integer.parseInt(jobParams.get(SOURCE_ID).toString()); + Source source = this.sourceRepository.findOne(sourceId); + if (source != null) { + return mapSource(source, cohortDefinitionId); + } else { + return 0; + } + } else { + return this.sourceRepository.findAll().stream() + .filter(source-> source.getDaimons() + .stream() + .anyMatch(daimon -> daimon.getDaimonType() == SourceDaimon.DaimonType.Results)) + .mapToInt(source -> mapSource(source, cohortDefinitionId)) + .sum(); + } + } + + private int mapSource(Source source, int cohortDefinitionId) { + try { + String resultSchema = source.getTableQualifier(SourceDaimon.DaimonType.Results); + return transactionTemplate.execute(transactionStatus -> { + List samples = sampleRepository.findByCohortDefinitionIdAndSourceId(cohortDefinitionId, source.getId()); + if (samples.isEmpty()) { + return 0; + } + + sampleRepository.delete(samples); + + int[] cohortSampleIds = samples.stream() + .mapToInt(CohortSample::getId) + .toArray(); + + PreparedStatementRenderer renderer = new PreparedStatementRenderer( + source, + "/resources/cohortsample/sql/deleteSampleElementsById.sql", + "results_schema", + resultSchema, + "cohortSampleId", + cohortSampleIds); + + samplingService.getSourceJdbcTemplate(source) + .update(renderer.getSql(), renderer.getOrderedParams()); + return cohortSampleIds.length; + }); + } catch (Exception e) { + log.error("Error deleting samples for cohort: {}, cause: {}", cohortDefinitionId, e.getMessage()); + return 0; + } + } + + @Override + public RepeatStatus execute(final StepContribution contribution, final ChunkContext chunkContext) throws Exception { + this.transactionTemplate.execute(status -> doTask(chunkContext)); + + return RepeatStatus.FINISHED; + } + + public JobExecutionResource launch(JobBuilderFactory jobBuilders, StepBuilderFactory stepBuilders, JobTemplate jobTemplate, int cohortDefinitionId) { + JobParametersBuilder builder = new JobParametersBuilder(); + builder.addString(JOB_NAME, String.format("Cleanup cohort samples of cohort definition %d.", cohortDefinitionId)); + builder.addString(COHORT_DEFINITION_ID, String.valueOf(cohortDefinitionId)); + + log.info("Beginning cohort cleanup for cohort definition id: {}", cohortDefinitionId); + return launch(jobBuilders, stepBuilders, jobTemplate, builder.toJobParameters()); + } + + public JobExecutionResource launch(JobBuilderFactory jobBuilders, StepBuilderFactory stepBuilders, JobTemplate jobTemplate, int cohortDefinitionId, int sourceId) { + JobParametersBuilder builder = new JobParametersBuilder(); + builder.addString(JOB_NAME, String.format("Cleanup cohort samples of cohort definition %d.", cohortDefinitionId)); + builder.addString(COHORT_DEFINITION_ID, String.valueOf(cohortDefinitionId)); + builder.addString(SOURCE_ID, String.valueOf(sourceId)); + + log.info("Beginning cohort cleanup for cohort definition id {} and source ID {}", cohortDefinitionId, sourceId); + return launch(jobBuilders, stepBuilders, jobTemplate, builder.toJobParameters()); + } + + private JobExecutionResource launch(JobBuilderFactory jobBuilders, StepBuilderFactory stepBuilders, JobTemplate jobTemplate, JobParameters jobParameters) { + Step cleanupStep = stepBuilders.get("cohortSample.cleanupSamples") + .tasklet(this) + .build(); + + SimpleJobBuilder cleanupJobBuilder = jobBuilders.get("cleanupSamples") + .start(cleanupStep); + + Job cleanupCohortJob = cleanupJobBuilder.build(); + + return jobTemplate.launch(cleanupCohortJob, jobParameters); + } +} diff --git a/src/main/java/org/ohdsi/webapi/cohortsample/CohortSample.java b/src/main/java/org/ohdsi/webapi/cohortsample/CohortSample.java new file mode 100644 index 0000000000..4bacd82e4f --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/cohortsample/CohortSample.java @@ -0,0 +1,139 @@ +package org.ohdsi.webapi.cohortsample; + +import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.Parameter; +import org.ohdsi.webapi.model.CommonEntity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Transient; +import java.util.List; + +/** + * Cohort sample details. + */ +@Entity(name = "CohortSample") +@Table(name = "cohort_sample") +public class CohortSample extends CommonEntity { + @Id + @GenericGenerator( + name = "cohort_sample_generator", + strategy = "org.hibernate.id.enhanced.SequenceStyleGenerator", + parameters = { + @Parameter(name = "sequence_name", value = "cohort_sample_sequence"), + @Parameter(name = "increment_size", value = "1") + } + ) + @GeneratedValue(generator = "cohort_sample_generator") + private Integer id; + + @Column + private String name; + + @Column(name = "cohort_definition_id") + private int cohortDefinitionId; + + @Column(name = "source_id") + private int sourceId; + + @Column(name = "age_mode") + private String ageMode; + + @Column(name = "age_min") + private Integer ageMin; + + @Column(name = "age_max") + private Integer ageMax; + + @Column(name = "gender_concept_ids") + private String genderConceptIds; + + @Column + private int size; + + @Transient + private List elements; + + public Integer getId() { + return this.id; + } + + public void setId(Integer id) { + this.id = id; + } + + public int getCohortDefinitionId() { + return cohortDefinitionId; + } + + public void setCohortDefinitionId(int cohortDefinitionId) { + this.cohortDefinitionId = cohortDefinitionId; + } + + public List getElements() { + return elements; + } + + public void setElements(List elements) { + this.elements = elements; + } + + public int getSize() { + return size; + } + + public void setSize(int size) { + this.size = size; + } + + public Integer getAgeMin() { + return ageMin; + } + + public void setAgeMin(Integer ageMin) { + this.ageMin = ageMin; + } + + public Integer getAgeMax() { + return ageMax; + } + + public void setAgeMax(Integer ageMax) { + this.ageMax = ageMax; + } + + public String getGenderConceptIds() { + return genderConceptIds; + } + + public void setGenderConceptIds(String genderConceptIds) { + this.genderConceptIds = genderConceptIds; + } + + public int getSourceId() { + return sourceId; + } + + public void setSourceId(int sourceId) { + this.sourceId = sourceId; + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public String getAgeMode() { + return ageMode; + } + + public void setAgeMode(String ageMode) { + this.ageMode = ageMode; + } +} diff --git a/src/main/java/org/ohdsi/webapi/cohortsample/CohortSampleRepository.java b/src/main/java/org/ohdsi/webapi/cohortsample/CohortSampleRepository.java new file mode 100644 index 0000000000..d04aa4d941 --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/cohortsample/CohortSampleRepository.java @@ -0,0 +1,26 @@ +package org.ohdsi.webapi.cohortsample; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * Repository of samples. This does not fetch any sample elements. + */ +@Component +public interface CohortSampleRepository extends CrudRepository { + @Query("SELECT c FROM CohortSample c LEFT JOIN FETCH c.createdBy WHERE c.id = :id") + CohortSample findById(@Param("id") int cohortSampleId); + + @Query("SELECT c FROM CohortSample c LEFT JOIN FETCH c.createdBy WHERE c.cohortDefinitionId = :cohortDefinitionId AND c.sourceId = :sourceId") + List findByCohortDefinitionIdAndSourceId(@Param("cohortDefinitionId") int cohortDefinitionId, @Param("sourceId") int sourceId); + + @Query("SELECT count(c.id) FROM CohortSample c WHERE c.cohortDefinitionId = :cohortDefinitionId") + int countSamples(@Param("cohortDefinitionId") int cohortDefinitionId); + + @Query("SELECT count(c.id) FROM CohortSample c WHERE c.cohortDefinitionId = :cohortDefinitionId AND c.sourceId = :sourceId") + int countSamples(@Param("cohortDefinitionId") int cohortDefinitionId, @Param("sourceId") int sourceId); +} diff --git a/src/main/java/org/ohdsi/webapi/cohortsample/CohortSamplingService.java b/src/main/java/org/ohdsi/webapi/cohortsample/CohortSamplingService.java new file mode 100644 index 0000000000..eb47bd851e --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/cohortsample/CohortSamplingService.java @@ -0,0 +1,511 @@ +package org.ohdsi.webapi.cohortsample; + +import org.ohdsi.webapi.cohortsample.dto.CohortSampleDTO; +import org.ohdsi.webapi.cohortsample.dto.SampleElementDTO; +import org.ohdsi.webapi.cohortsample.dto.SampleParametersDTO; +import org.ohdsi.webapi.job.JobTemplate; +import org.ohdsi.webapi.service.AbstractDaoService; +import org.ohdsi.webapi.shiro.Entities.UserEntity; +import org.ohdsi.webapi.source.Source; +import org.ohdsi.webapi.source.SourceDaimon; +import org.ohdsi.webapi.user.dto.UserDTO; +import org.ohdsi.webapi.util.PreparedStatementRenderer; +import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; +import org.springframework.batch.core.configuration.annotation.StepBuilderFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; +import org.springframework.transaction.support.TransactionCallback; + +import javax.ws.rs.BadRequestException; +import javax.ws.rs.NotFoundException; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.ohdsi.sql.SqlTranslate; + +import static org.ohdsi.webapi.cohortsample.dto.SampleParametersDTO.GenderDTO.GENDER_FEMALE_CONCEPT_ID; +import static org.ohdsi.webapi.cohortsample.dto.SampleParametersDTO.GenderDTO.GENDER_MALE_CONCEPT_ID; +import org.ohdsi.webapi.util.SourceUtils; + +/** + * Service to do manage samples of a cohort definition. + */ +@Component +public class CohortSamplingService extends AbstractDaoService { + private final CohortSampleRepository sampleRepository; + private final JobBuilderFactory jobBuilders; + private final StepBuilderFactory stepBuilders; + private final JobTemplate jobTemplate; + + @Autowired + public CohortSamplingService( + CohortSampleRepository sampleRepository, + JobBuilderFactory jobBuilders, + StepBuilderFactory stepBuilders, + JobTemplate jobTemplate) { + this.sampleRepository = sampleRepository; + this.jobBuilders = jobBuilders; + this.stepBuilders = stepBuilders; + this.jobTemplate = jobTemplate; + } + + public List listSamples(int cohortDefinitionId, int sourceId) { + return sampleRepository.findByCohortDefinitionIdAndSourceId(cohortDefinitionId, sourceId).stream() + .map(sample -> sampleToSampleDTO(sample, null, false)) + .collect(Collectors.toList()); + } + + public CohortSampleDTO getSample(int sampleId, boolean withRecordCounts) { + CohortSample sample = sampleRepository.findById(sampleId); + if (sample == null) { + throw new NotFoundException("Cohort sample with ID " + sampleId + " not found"); + } + Source source = getSourceRepository().findBySourceId(sample.getSourceId()); + List sampleElements = findSampleElements(source, sample.getId(), withRecordCounts); + return sampleToSampleDTO(sample, sampleElements, true); + } + + public int countSamples(int cohortDefinitionId) { + return sampleRepository.countSamples(cohortDefinitionId); + } + + public int countSamples(int cohortDefinitionId, int sourceId) { + return sampleRepository.countSamples(cohortDefinitionId, sourceId); + } + + /** + * Find all sample elements of a sample. + * @param source Source to use + * @param cohortSampleId sample ID of the elements. + * @param withRecordCounts whether to return record counts. This makes the query much slower. + * @return list of elements. + */ + private List findSampleElements(Source source, int cohortSampleId, boolean withRecordCounts) { + JdbcTemplate jdbcTemplate = getSourceJdbcTemplate(source); + PreparedStatementRenderer renderer; + Collection optionalFields; + + if (withRecordCounts) { + renderer = new PreparedStatementRenderer(source, "/resources/cohortsample/sql/findElementsByCohortSampleIdWithCounts.sql", + new String[]{"results_schema", "CDM_schema"}, + new String[]{source.getTableQualifier(SourceDaimon.DaimonType.Results), source.getTableQualifier(SourceDaimon.DaimonType.CDM)}, + "cohortSampleId", cohortSampleId); + optionalFields = Collections.singleton("record_count"); + } else { + renderer = new PreparedStatementRenderer(source, "/resources/cohortsample/sql/findElementsByCohortSampleId.sql", + "results_schema", + source.getTableQualifier(SourceDaimon.DaimonType.Results), + "cohortSampleId", cohortSampleId); + optionalFields = Collections.emptySet(); + } + return jdbcTemplate.query(renderer.getSql(), renderer.getOrderedParams(), new CohortSampleElementRowMapper(optionalFields)); + } + + /** + * Create a new sample in given source and cohort definition, using sample parameters. + * @param source Source to use + * @param cohortDefinitionId cohort definition ID to sample + * @param sampleParameters parameters to define the sample + * @return list of elements. + */ + public CohortSampleDTO createSample(Source source, int cohortDefinitionId, SampleParametersDTO sampleParameters) { + JdbcTemplate jdbcTemplate = getSourceJdbcTemplate(source); + + CohortSample sample = new CohortSample(); + sample.setName(sampleParameters.getName()); + sample.setCohortDefinitionId(cohortDefinitionId); + sample.setSourceId(source.getId()); + sample.setSize(sampleParameters.getSize()); + + SampleParametersDTO.AgeDTO age = sampleParameters.getAge(); + if (age != null) { + switch (age.getMode()) { + case LESS_THAN: + case LESS_THAN_OR_EQUAL: + sample.setAgeMax(age.getValue()); + break; + case GREATER_THAN: + case GREATER_THAN_OR_EQUAL: + sample.setAgeMin(age.getValue()); + break; + case EQUAL_TO: + sample.setAgeMin(age.getValue()); + sample.setAgeMax(age.getValue()); + break; + case BETWEEN: + case NOT_BETWEEN: + sample.setAgeMin(age.getMin()); + sample.setAgeMax(age.getMax()); + break; + } + sample.setAgeMode(age.getMode().getSerialName()); + } + + SampleParametersDTO.GenderDTO gender = sampleParameters.getGender(); + if (gender != null) { + StringBuilder sb = new StringBuilder(12); + for (Integer conceptId : gender.getConceptIds()) { + if (sb.length() > 0) { + sb.append(','); + } + sb.append(conceptId); + } + if (gender.isOtherNonBinary()) { + if (sb.length() > 0) { + sb.append(','); + } + sb.append(-1); + } + sample.setGenderConceptIds(sb.toString()); + } + sample.setCreatedBy(getCurrentUser()); + sample.setCreatedDate(new Date()); + + log.info("Sampling {} elements for cohort {}", sampleParameters.getSize(), cohortDefinitionId); + final List elements = sampleElements(sampleParameters, sample, jdbcTemplate, source); + + if (elements.size() < sample.getSize()) { + sample.setSize(elements.size()); + } + + getTransactionTemplate().execute((TransactionCallback) transactionStatus -> { + log.debug("Saving {} sample elements for cohort {}", sample.getSize(), cohortDefinitionId); + CohortSample updatedSample = sampleRepository.save(sample); + insertSampledElements(source, jdbcTemplate, updatedSample.getId(), elements); + + return null; + }); + + return sampleToSampleDTO(sample, elements, true); + } + + /** + * Create a new sample in given source and cohort definition, using sample parameters. + * @param sampleId The sample to refresh + */ + public void refreshSample(Integer sampleId) { + + CohortSample sample = sampleRepository.findById(sampleId); + if (sample == null) { + throw new NotFoundException("Cohort sample with ID " + sampleId + " not found"); + } + Source source = getSourceRepository().findBySourceId(sample.getSourceId()); + + CohortSampleDTO sampleDto = sampleToSampleDTO(sample, null, true); + SampleParametersDTO sampleParamaters = new SampleParametersDTO(); + sampleParamaters.setAge(sampleDto.getAge()); + sampleParamaters.setGender(sampleDto.getGender()); + sampleParamaters.setSize(sampleDto.getSize()); + log.info("Sampling {} elements for cohort {}", sampleParamaters.getSize(), sample.getCohortDefinitionId()); + JdbcTemplate jdbcTemplate = getSourceJdbcTemplate(source); + final List elements = sampleElements(sampleParamaters, sample, jdbcTemplate, source); + + getTransactionTemplate().execute((TransactionCallback) transactionStatus -> { + String deleteSql = String.format( + "DELETE FROM %s.cohort_sample_element WHERE cohort_sample_id = %d;", + source.getTableQualifier(SourceDaimon.DaimonType.Results), + sample.getId()); + String translatedDeleteSql = SqlTranslate.translateSql(deleteSql, source.getSourceDialect(), null, null); + jdbcTemplate.update(translatedDeleteSql); + insertSampledElements(source, jdbcTemplate, sample.getId(), elements); + return null; + }); + } + + /** Convert a given sample with given elements to a DTO. */ + private CohortSampleDTO sampleToSampleDTO(CohortSample sample, List elements, boolean includeIds) { + CohortSampleDTO sampleDTO = new CohortSampleDTO(); + sampleDTO.setId(sample.getId()); + sampleDTO.setName(sample.getName()); + sampleDTO.setSize(sample.getSize()); + if (includeIds) { + sampleDTO.setCohortDefinitionId(sample.getCohortDefinitionId()); + sampleDTO.setSourceId(sample.getSourceId()); + } + sampleDTO.setCreatedDate(sample.getCreatedDate()); + UserEntity createdBy = sample.getCreatedBy(); + if (createdBy != null) { + UserDTO userDto = new UserDTO(); + userDto.setId(createdBy.getId()); + userDto.setLogin(createdBy.getLogin()); + userDto.setName(createdBy.getName()); + sampleDTO.setCreatedBy(userDto); + } + + SampleParametersDTO.AgeMode ageMode = SampleParametersDTO.AgeMode.fromSerialName(sample.getAgeMode()); + if (ageMode != null) { + SampleParametersDTO.AgeDTO age = new SampleParametersDTO.AgeDTO(); + age.setMode(ageMode); + switch (ageMode) { + case LESS_THAN: + case LESS_THAN_OR_EQUAL: + case EQUAL_TO: + age.setValue(sample.getAgeMax()); + break; + case GREATER_THAN: + case GREATER_THAN_OR_EQUAL: + age.setValue(sample.getAgeMin()); + break; + case BETWEEN: + case NOT_BETWEEN: + age.setMin(sample.getAgeMin()); + age.setMax(sample.getAgeMax()); + break; + } + sampleDTO.setAge(age); + } + if (sample.getGenderConceptIds() != null && !sample.getGenderConceptIds().isEmpty()) { + List conceptIds = Arrays.stream(sample.getGenderConceptIds().split(",")) + .map(Integer::valueOf) + .collect(Collectors.toList()); + + SampleParametersDTO.GenderDTO genderDto = new SampleParametersDTO.GenderDTO(); + + if (conceptIds.remove(Integer.valueOf(-1))) { + genderDto.setOtherNonBinary(true); + } + + genderDto.setConceptIds(conceptIds); + sampleDTO.setGender(genderDto); + } + + sampleDTO.setElements(sampleElementToDTO(elements)); + return sampleDTO; + } + + /** Convert given sample elements DTOs. */ + private List sampleElementToDTO(List elements) { + if (elements == null) { + return null; + } + + return elements.stream() + .map(el -> { + SampleElementDTO elementDTO = new SampleElementDTO(); + elementDTO.setRank(el.getRank()); + elementDTO.setPersonId(String.valueOf(el.getPersonId())); + elementDTO.setAge(el.getAge()); + elementDTO.setGenderConceptId(el.getGenderConceptId()); + elementDTO.setRecordCount(el.getRecordCount()); + return elementDTO; + }) + .collect(Collectors.toList()); + } + + /** Insert elements that have been sampled. */ + private void insertSampledElements(Source source, JdbcTemplate jdbcTemplate, int sampleId, List elements) { + if (elements.isEmpty()) { + return; + } + + String[] parameters = new String[] { "results_schema" }; + String[] parameterValues = new String[] { source.getTableQualifier(SourceDaimon.DaimonType.Results) }; + String[] sqlParameters = new String[] { "cohortSampleId", "rank", "personId", "age", "genderConceptId" }; + + String statement = null; + List variables = new ArrayList<>(elements.size()); + for (SampleElement element : elements) { + Object[] sqlValues = new Object[] { + sampleId, + element.getRank(), + element.getPersonId(), + element.getAge(), + element.getGenderConceptId() }; + + PreparedStatementRenderer renderer = new PreparedStatementRenderer(source, "/resources/cohortsample/sql/insertSampleElement.sql", parameters, parameterValues, sqlParameters, sqlValues); + + if (statement == null) { + statement = renderer.getSql(); + } + + variables.add(renderer.getOrderedParams()); + } + + jdbcTemplate.batchUpdate(statement, variables); + } + + /** Sample elements based on parameters. */ + private List sampleElements(SampleParametersDTO sampleParametersDTO, CohortSample sample, JdbcTemplate jdbcTemplate, Source source) { + StringBuilder expressionBuilder = new StringBuilder(); + Map sqlVariables = new LinkedHashMap<>(); + + sqlVariables.put("cohort_definition_id", sample.getCohortDefinitionId()); + + SampleParametersDTO.AgeMode ageMode = SampleParametersDTO.AgeMode.fromSerialName(sample.getAgeMode()); + + if (ageMode != null) { + switch (ageMode) { + case LESS_THAN: + expressionBuilder.append(" AND age < @age_max"); + sqlVariables.put("age_max", sample.getAgeMax()); + break; + case LESS_THAN_OR_EQUAL: + expressionBuilder.append(" AND age <= @age_max"); + sqlVariables.put("age_max", sample.getAgeMax()); + break; + case GREATER_THAN: + expressionBuilder.append(" AND age > @age_min"); + sqlVariables.put("age_min", sample.getAgeMin()); + break; + case GREATER_THAN_OR_EQUAL: + expressionBuilder.append(" AND age >= @age_min"); + sqlVariables.put("age_min", sample.getAgeMin()); + break; + case EQUAL_TO: + expressionBuilder.append(" AND age = @age_equal"); + sqlVariables.put("age_equal", sample.getAgeMin()); + break; + case BETWEEN: + expressionBuilder.append(" AND age <= @age_max AND age >= @age_min"); + sqlVariables.put("age_min", sample.getAgeMin()); + sqlVariables.put("age_max", sample.getAgeMax()); + break; + case NOT_BETWEEN: + expressionBuilder.append(" AND age > @age_max OR age < @age_min"); + sqlVariables.put("age_min", sample.getAgeMin()); + sqlVariables.put("age_max", sample.getAgeMax()); + break; + } + } + + SampleParametersDTO.GenderDTO gender = sampleParametersDTO.getGender(); + if (gender != null) { + List conceptIds = gender.getConceptIds(); + if (gender.isOtherNonBinary()) { + if (conceptIds.size() == 0) { + expressionBuilder.append(" AND gender_concept_id NOT IN (") + .append(GENDER_MALE_CONCEPT_ID) + .append(',') + .append(GENDER_FEMALE_CONCEPT_ID) + .append(')'); + } else if (conceptIds.size() == 1) { + if (conceptIds.get(0) == GENDER_FEMALE_CONCEPT_ID) { + expressionBuilder.append(" AND gender_concept_id <> ") + .append(GENDER_MALE_CONCEPT_ID); + + } else { + expressionBuilder.append(" AND gender_concept_id <> ") + .append(GENDER_FEMALE_CONCEPT_ID); + } + } + // else: all genders are selected, no where statement needed + } else if (!conceptIds.isEmpty()) { + expressionBuilder.append(" AND gender_concept_id IN ("); + for (int i = 0; i < conceptIds.size(); i++) { + if (i > 0) { + expressionBuilder.append(','); + } + expressionBuilder.append(conceptIds.get(i)); + } + expressionBuilder.append(')'); + } + // else: all genders are selected, no where statement needed + } + + String[] parameterKeys = new String[] { "results_schema", "CDM_schema", "expression"}; + String[] parameterValues = new String[] { + source.getTableQualifier(SourceDaimon.DaimonType.Results), + source.getTableQualifier(SourceDaimon.DaimonType.CDM), + expressionBuilder.toString() }; + String[] sqlVariableKeys = sqlVariables.keySet().toArray(new String[0]); + Object[] sqlVariableValues = Stream.of(sqlVariableKeys) + .map(sqlVariables::get) + .toArray(Object[]::new); + + PreparedStatementRenderer renderer = new PreparedStatementRenderer(source, "/resources/cohortsample/sql/generateSample.sql", + parameterKeys, + parameterValues, + sqlVariableKeys, + sqlVariableValues); + + jdbcTemplate.setMaxRows(sample.getSize()); + + return jdbcTemplate.query(renderer.getSql(), renderer.getOrderedParams(), (rs, rowNum) -> { + SampleElement element = new SampleElement(); + element.setRank(rowNum); + element.setAge(rs.getInt("age")); + element.setGenderConceptId(rs.getInt("gender_concept_id")); + element.setPersonId(rs.getLong("person_id")); + return element; + }); + } + + /** Delete a sample and its elements. */ + public void deleteSample(int cohortDefinitionId, Source source, int sampleId) { + JdbcTemplate jdbcTemplate = getSourceJdbcTemplate(source); + String resultsSchema = source.getTableQualifier(SourceDaimon.DaimonType.Results); + String sql = new PreparedStatementRenderer( + source, + "/resources/cohortsample/sql/deleteSampleElementsById.sql", + "results_schema", + resultsSchema, + "cohortSampleId", + sampleId).getSql(); + CohortSample sample = sampleRepository.findOne(sampleId); + if (sample == null) { + throw new NotFoundException("Sample with ID " + sampleId + " does not exist"); + } + if (sample.getCohortDefinitionId() != cohortDefinitionId) { + throw new BadRequestException("Cohort definition ID " + sample.getCohortDefinitionId() + " does not match provided cohort definition id " + cohortDefinitionId); + } + if (sample.getSourceId() != source.getId()) { + throw new BadRequestException("Source " + sample.getSourceId() + " does not match provided source " + source.getId()); + } + + getTransactionTemplate().execute((TransactionCallback) transactionStatus -> { + sampleRepository.delete(sampleId); + jdbcTemplate.update(sql, sampleId); + return null; + }); + } + + public void launchDeleteSamplesTasklet(int cohortDefinitionId) { + CleanupCohortSamplesTasklet tasklet = createDeleteSamplesTasklet(); + + tasklet.launch(jobBuilders, stepBuilders, jobTemplate, cohortDefinitionId); + } + + public void launchDeleteSamplesTasklet(int cohortDefinitionId, int sourceId) { + CleanupCohortSamplesTasklet tasklet = createDeleteSamplesTasklet(); + + tasklet.launch(jobBuilders, stepBuilders, jobTemplate, cohortDefinitionId, sourceId); + } + + public CleanupCohortSamplesTasklet createDeleteSamplesTasklet() { + return new CleanupCohortSamplesTasklet(getTransactionTemplate(), getSourceRepository(), this, sampleRepository); + } + + /** Maps a SQL result to a sample element. */ + private static class CohortSampleElementRowMapper implements RowMapper { + private final Collection optionalFields; + + CohortSampleElementRowMapper(Collection optionalFields) { + this.optionalFields = optionalFields; + } + + @Override + public SampleElement mapRow(ResultSet rs, int rowNum) throws SQLException { + SampleElement sample = new SampleElement(); + sample.setRank(rs.getInt("rank_value")); + sample.setSampleId(rs.getInt("cohort_sample_id")); + sample.setPersonId(rs.getLong("person_id")); + sample.setGenderConceptId(rs.getInt("gender_concept_id")); + sample.setAge(rs.getInt("age")); + if (optionalFields.contains("record_count")) { + sample.setRecordCount(rs.getInt("record_count")); + } + return sample; + } + } +} diff --git a/src/main/java/org/ohdsi/webapi/cohortsample/SampleElement.java b/src/main/java/org/ohdsi/webapi/cohortsample/SampleElement.java new file mode 100644 index 0000000000..7a2d506899 --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/cohortsample/SampleElement.java @@ -0,0 +1,64 @@ +package org.ohdsi.webapi.cohortsample; + +/** A single person that is part of a given sample. */ +public class SampleElement { + private int sampleId; + + private int rank; + + private long personId; + + private long genderConceptId; + + private int age; + + private Integer recordCount; + + public int getSampleId() { + return sampleId; + } + + public void setSampleId(int sampleId) { + this.sampleId = sampleId; + } + + public int getRank() { + return rank; + } + + public void setRank(int rank) { + this.rank = rank; + } + + public long getPersonId() { + return personId; + } + + public void setPersonId(long personId) { + this.personId = personId; + } + + public long getGenderConceptId() { + return genderConceptId; + } + + public void setGenderConceptId(long genderConceptId) { + this.genderConceptId = genderConceptId; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public Integer getRecordCount() { + return recordCount; + } + + public void setRecordCount(Integer recordCount) { + this.recordCount = recordCount; + } +} diff --git a/src/main/java/org/ohdsi/webapi/cohortsample/dto/CohortSampleDTO.java b/src/main/java/org/ohdsi/webapi/cohortsample/dto/CohortSampleDTO.java new file mode 100644 index 0000000000..0ef3b9fa5f --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/cohortsample/dto/CohortSampleDTO.java @@ -0,0 +1,134 @@ +package org.ohdsi.webapi.cohortsample.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import org.ohdsi.webapi.user.dto.UserDTO; + +import java.util.Date; +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class CohortSampleDTO { + /** Cohort sample ID. */ + private int id; + /** Cohort sample name. */ + private String name; + /** + * Actual sample size. This may be different from the size specified by the user if not enough + * persons could be found matching the criteria. + */ + private int size; + + /** + * Date that the sample was created. + */ + private Date createdDate; + + /** + * User that created the sample. If no login system is used, this is null. + */ + private UserDTO createdBy; + + /** + * Cohort definition ID that was sampled. + */ + private Integer cohortDefinitionId; + /** + * Source ID that was sampled. + */ + private Integer sourceId; + + /** + * Age criteria used to create the sample. + */ + private SampleParametersDTO.AgeDTO age; + + /** + * Gender criteria used to create the sample. + */ + private SampleParametersDTO.GenderDTO gender; + + /** + * Actually sampled elements. + */ + private List elements; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public Date getCreatedDate() { + return createdDate; + } + + public void setCreatedDate(Date createdDate) { + this.createdDate = createdDate; + } + + public UserDTO getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(UserDTO createdBy) { + this.createdBy = createdBy; + } + + public Integer getCohortDefinitionId() { + return cohortDefinitionId; + } + + public void setCohortDefinitionId(Integer cohortDefinitionId) { + this.cohortDefinitionId = cohortDefinitionId; + } + + public Integer getSourceId() { + return sourceId; + } + + public void setSourceId(Integer sourceId) { + this.sourceId = sourceId; + } + + public int getSize() { + return size; + } + + public void setSize(int size) { + this.size = size; + } + + public List getElements() { + return elements; + } + + public void setElements(List elements) { + this.elements = elements; + } + + public SampleParametersDTO.AgeDTO getAge() { + return age; + } + + public void setAge(SampleParametersDTO.AgeDTO age) { + this.age = age; + } + + public SampleParametersDTO.GenderDTO getGender() { + return gender; + } + + public void setGender(SampleParametersDTO.GenderDTO gender) { + this.gender = gender; + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/org/ohdsi/webapi/cohortsample/dto/CohortSampleListDTO.java b/src/main/java/org/ohdsi/webapi/cohortsample/dto/CohortSampleListDTO.java new file mode 100644 index 0000000000..b16b90a0e3 --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/cohortsample/dto/CohortSampleListDTO.java @@ -0,0 +1,53 @@ +package org.ohdsi.webapi.cohortsample.dto; + +import org.ohdsi.webapi.GenerationStatus; + +import java.util.List; + +public class CohortSampleListDTO { + private int cohortDefinitionId; + private int sourceId; + private GenerationStatus generationStatus; + private boolean isValid; + private List samples; + + public int getCohortDefinitionId() { + return cohortDefinitionId; + } + + public void setCohortDefinitionId(int cohortDefinitionId) { + this.cohortDefinitionId = cohortDefinitionId; + } + + public int getSourceId() { + return sourceId; + } + + public void setSourceId(int sourceId) { + this.sourceId = sourceId; + } + + public GenerationStatus getGenerationStatus() { + return generationStatus; + } + + public void setGenerationStatus(GenerationStatus generationStatus) { + this.generationStatus = generationStatus; + } + + public boolean isValid() { + return isValid; + } + + public void setIsValid(boolean valid) { + isValid = valid; + } + + public List getSamples() { + return samples; + } + + public void setSamples(List samples) { + this.samples = samples; + } +} diff --git a/src/main/java/org/ohdsi/webapi/cohortsample/dto/SampleElementDTO.java b/src/main/java/org/ohdsi/webapi/cohortsample/dto/SampleElementDTO.java new file mode 100644 index 0000000000..4191f01569 --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/cohortsample/dto/SampleElementDTO.java @@ -0,0 +1,82 @@ +package org.ohdsi.webapi.cohortsample.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class SampleElementDTO { + /** + * Sample ID that this element belongs to. May be null if this object is part of a + * {@link CohortSampleDTO} object. + */ + private Integer sampleId; + + /** + * Rank of the object within the sample. This establishes order between elements. + */ + private int rank; + + /** + * Person ID of the element. + */ + private String personId; + + /** + * Gender ID of the person. + */ + private long genderConceptId; + + /** + * Age of the person. + */ + private int age; + + private Integer recordCount; + + public Integer getSampleId() { + return sampleId; + } + + public void setSampleId(Integer sampleId) { + this.sampleId = sampleId; + } + + public int getRank() { + return rank; + } + + public void setRank(int rank) { + this.rank = rank; + } + + public String getPersonId() { + return personId; + } + + public void setPersonId(String personId) { + this.personId = personId; + } + + public long getGenderConceptId() { + return genderConceptId; + } + + public void setGenderConceptId(long genderConceptId) { + this.genderConceptId = genderConceptId; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public Integer getRecordCount() { + return recordCount; + } + + public void setRecordCount(Integer recordCount) { + this.recordCount = recordCount; + } +} diff --git a/src/main/java/org/ohdsi/webapi/cohortsample/dto/SampleParametersDTO.java b/src/main/java/org/ohdsi/webapi/cohortsample/dto/SampleParametersDTO.java new file mode 100644 index 0000000000..d60322591a --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/cohortsample/dto/SampleParametersDTO.java @@ -0,0 +1,257 @@ +package org.ohdsi.webapi.cohortsample.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonValue; + +import javax.ws.rs.BadRequestException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +public class SampleParametersDTO { + private static final int SIZE_MAX = 500; + private static final int AGE_MAX = 500; + + /** Sample size. */ + private int size; + /** Sample name. */ + private String name; + + /** Gender criteria. */ + private GenderDTO gender; + + /** Age criteria. */ + private AgeDTO age; + + /** + * Validate this DTO. + * @throws BadRequestException if the DTO is not valid. + */ + public void validate() { + if (name == null) { + throw new BadRequestException("Sample must have a name"); + } + if (size <= 0) { + throw new BadRequestException("sample parameter size must fall in the range (1, " + SIZE_MAX + ")"); + } + if (size > SIZE_MAX) { + throw new BadRequestException("sample parameter size must fall in the range (1, " + SIZE_MAX + ")"); + } + if (age != null && !age.validate()) { + age = null; + } + if (gender != null && !gender.validate()) { + gender = null; + } + } + + public AgeDTO getAge() { + return age; + } + + public void setAge(AgeDTO age) { + this.age = age; + } + + public GenderDTO getGender() { + return gender; + } + + public void setGender(GenderDTO gender) { + this.gender = gender; + } + + public int getSize() { + return size; + } + + public void setSize(int size) { + this.size = size; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public enum AgeMode { + LESS_THAN("lessThan"), + LESS_THAN_OR_EQUAL("lessThanOrEqual"), + GREATER_THAN("greaterThan"), + GREATER_THAN_OR_EQUAL("greaterThanOrEqual"), + EQUAL_TO("equalTo"), + BETWEEN("between"), + NOT_BETWEEN("notBetween"); + + private final String serialName; + + AgeMode(String serialName) { + this.serialName = serialName; + } + + @JsonValue + public String getSerialName() { + return serialName; + } + + public static AgeMode fromSerialName(String name) { + return Stream.of(SampleParametersDTO.AgeMode.values()) + .filter(mode -> mode.getSerialName().equals(name)) + .findFirst() + .orElse(null); + } + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + public static class GenderDTO { + public final static int GENDER_MALE_CONCEPT_ID = 8507; + public final static int GENDER_FEMALE_CONCEPT_ID = 8532; + + private Integer conceptId; + + private List conceptIds; + + private boolean otherNonBinary = false; + + /** + * Validate this DTO. + * @return true if this DTO contains any information, false otherwise. + */ + public boolean validate() { + if (conceptIds == null) { + conceptIds = new ArrayList<>(); + } else if (conceptIds.contains(null)) { + conceptIds.removeIf(Objects::isNull); + } + if (conceptId != null) { + conceptIds.add(conceptId); + conceptId = null; + } + + if (!isOtherNonBinary() && conceptIds.isEmpty()) { + return false; + } + + if (isOtherNonBinary()) { + conceptIds.removeIf(i -> i != GENDER_MALE_CONCEPT_ID && i != GENDER_FEMALE_CONCEPT_ID); + } + return true; + } + + public Integer getConceptId() { + return conceptId; + } + + public void setConceptId(Integer conceptId) { + this.conceptId = conceptId; + } + + public List getConceptIds() { + return conceptIds; + } + + public void setConceptIds(List conceptIds) { + this.conceptIds = conceptIds; + } + + public boolean isOtherNonBinary() { + return otherNonBinary; + } + + public void setOtherNonBinary(boolean otherNonBinary) { + this.otherNonBinary = otherNonBinary; + } + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + public static class AgeDTO { + private Integer min; + private Integer max; + private Integer value; + private AgeMode mode; + + /** + * Validate this DTO. + * @return true if this DTO contains any information, false otherwise. + * @throws BadRequestException if the DTO is not valid. + */ + public boolean validate() { + if (mode == null) { + if (min != null || max != null || value != null) { + throw new BadRequestException("Cannot specify age without a mode to use age with."); + } else { + return false; + } + } + switch (mode) { + case LESS_THAN: + case LESS_THAN_OR_EQUAL: + case GREATER_THAN: + case GREATER_THAN_OR_EQUAL: + case EQUAL_TO: + if (value == null) { + throw new BadRequestException("Cannot use single age comparison mode " + mode.getSerialName() + " without age property."); + } + if (min != null || max != null) { + throw new BadRequestException("Cannot use age range property with comparison mode " + mode.getSerialName() + "."); + } + break; + case BETWEEN: + case NOT_BETWEEN: + if (min == null || max == null) { + throw new BadRequestException("Cannot use age range comparison mode " + mode.getSerialName() + " without ageMin and ageMax properties."); + } + if (value != null) { + throw new BadRequestException("Cannot use single age property with comparison mode " + mode.getSerialName() + "."); + } + if (min < 0) { + throw new BadRequestException("Minimum age may not be less than 0"); + } + if (max >= AGE_MAX) { + throw new BadRequestException("Maximum age must be smaller than " + AGE_MAX); + } + if (min > max) { + throw new BadRequestException("Maximum age " + max + " may not be less than minimum age " + min); + } + break; + } + return true; + } + + public Integer getMin() { + return min; + } + + public void setMin(Integer min) { + this.min = min; + } + + public Integer getMax() { + return max; + } + + public void setMax(Integer max) { + this.max = max; + } + + public Integer getValue() { + return value; + } + + public void setValue(Integer value) { + this.value = value; + } + + public AgeMode getMode() { + return mode; + } + + public void setMode(AgeMode mode) { + this.mode = mode; + } + } +} diff --git a/src/main/java/org/ohdsi/webapi/security/model/CohortSamplePermissionSchema.java b/src/main/java/org/ohdsi/webapi/security/model/CohortSamplePermissionSchema.java new file mode 100644 index 0000000000..12923eadff --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/security/model/CohortSamplePermissionSchema.java @@ -0,0 +1,21 @@ +package org.ohdsi.webapi.security.model; + +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +@Component +public class CohortSamplePermissionSchema extends EntityPermissionSchema { + private static Map readPermissions = new HashMap() {{ + put("cohortsample:*:*:%s:get", "Get Cohort Sample with ID = %s"); + }}; + + private static Map writePermissions = new HashMap() {{ + put("cohortsample:*:*:%s:delete", "Delete Cohort Sample with ID = %s"); + }}; + + public CohortSamplePermissionSchema() { + super(EntityType.COHORT_SAMPLE, readPermissions, writePermissions); + } +} diff --git a/src/main/java/org/ohdsi/webapi/security/model/EntityType.java b/src/main/java/org/ohdsi/webapi/security/model/EntityType.java index b48c895992..a87c4bdd85 100644 --- a/src/main/java/org/ohdsi/webapi/security/model/EntityType.java +++ b/src/main/java/org/ohdsi/webapi/security/model/EntityType.java @@ -2,6 +2,7 @@ import org.ohdsi.webapi.cohortcharacterization.domain.CohortCharacterizationEntity; import org.ohdsi.webapi.cohortdefinition.CohortDefinition; +import org.ohdsi.webapi.cohortsample.CohortSample; import org.ohdsi.webapi.conceptset.ConceptSet; import org.ohdsi.webapi.estimation.Estimation; import org.ohdsi.webapi.feanalysis.domain.FeAnalysisEntity; @@ -20,7 +21,8 @@ public enum EntityType { INCIDENCE_RATE(IncidenceRateAnalysis.class), SOURCE(Source.class), ESTIMATION(Estimation.class), - PREDICTION(PredictionAnalysis.class); + PREDICTION(PredictionAnalysis.class), + COHORT_SAMPLE(CohortSample.class); private final Class entityClass; diff --git a/src/main/java/org/ohdsi/webapi/service/CohortDefinitionService.java b/src/main/java/org/ohdsi/webapi/service/CohortDefinitionService.java index a99c6d4aba..01be555430 100644 --- a/src/main/java/org/ohdsi/webapi/service/CohortDefinitionService.java +++ b/src/main/java/org/ohdsi/webapi/service/CohortDefinitionService.java @@ -30,6 +30,8 @@ import org.ohdsi.webapi.cohortdefinition.dto.CohortDTO; import org.ohdsi.webapi.cohortdefinition.dto.CohortGenerationInfoDTO; import org.ohdsi.webapi.cohortdefinition.dto.CohortMetadataDTO; +import org.ohdsi.webapi.cohortsample.CleanupCohortSamplesTasklet; +import org.ohdsi.webapi.cohortsample.CohortSamplingService; import org.ohdsi.webapi.cohortdefinition.dto.CohortRawDTO; import org.ohdsi.webapi.cohortdefinition.event.CohortDefinitionChangedEvent; import org.ohdsi.webapi.common.SourceMapKey; @@ -148,6 +150,9 @@ public class CohortDefinitionService extends AbstractDaoService { @Autowired private ObjectMapper objectMapper; + + @Autowired + private CohortSamplingService samplingService; @Autowired private ApplicationEventPublisher eventPublisher; @@ -595,6 +600,7 @@ public void doInTransactionWithoutResult(final TransactionStatus status) { }); }); cohortDefinitionRepository.delete(def); + samplingService.launchDeleteSamplesTasklet(id); } else { log.warn("Failed to delete Cohort Definition with ID = {}", id); } @@ -615,8 +621,15 @@ public void doInTransactionWithoutResult(final TransactionStatus status) { .tasklet(cleanupTasklet) .build(); + CleanupCohortSamplesTasklet cleanupSamplesTasklet = samplingService.createDeleteSamplesTasklet(); + + Step cleanupSamplesStep = stepBuilders.get("cohortDefinition.cleanupSamples") + .tasklet(cleanupSamplesTasklet) + .build(); + SimpleJobBuilder cleanupJobBuilder = jobBuilders.get("cleanupCohort") - .start(cleanupStep); + .start(cleanupStep) + .next(cleanupSamplesStep); Job cleanupCohortJob = cleanupJobBuilder.build(); diff --git a/src/main/java/org/ohdsi/webapi/service/CohortSampleService.java b/src/main/java/org/ohdsi/webapi/service/CohortSampleService.java new file mode 100644 index 0000000000..7120d17b1e --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/service/CohortSampleService.java @@ -0,0 +1,176 @@ +package org.ohdsi.webapi.service; + +import org.ohdsi.webapi.GenerationStatus; +import org.ohdsi.webapi.cohortdefinition.CohortDefinitionRepository; +import org.ohdsi.webapi.cohortdefinition.CohortGenerationInfo; +import org.ohdsi.webapi.cohortdefinition.CohortGenerationInfoId; +import org.ohdsi.webapi.cohortdefinition.CohortGenerationInfoRepository; +import org.ohdsi.webapi.cohortsample.CohortSamplingService; +import org.ohdsi.webapi.cohortsample.dto.CohortSampleDTO; +import org.ohdsi.webapi.cohortsample.dto.CohortSampleListDTO; +import org.ohdsi.webapi.cohortsample.dto.SampleParametersDTO; +import org.ohdsi.webapi.source.Source; +import org.ohdsi.webapi.source.SourceRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.ws.rs.BadRequestException; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.*; + +@Path("/cohortsample") +@Component +@Produces(MediaType.APPLICATION_JSON) +public class CohortSampleService { + private final CohortDefinitionRepository cohortDefinitionRepository; + private final CohortGenerationInfoRepository generationInfoRepository; + private final CohortSamplingService samplingService; + private final SourceRepository sourceRepository; + + @Autowired + public CohortSampleService( + CohortSamplingService samplingService, + SourceRepository sourceRepository, + CohortDefinitionRepository cohortDefinitionRepository, + CohortGenerationInfoRepository generationInfoRepository + ) { + this.samplingService = samplingService; + this.sourceRepository = sourceRepository; + this.cohortDefinitionRepository = cohortDefinitionRepository; + this.generationInfoRepository = generationInfoRepository; + } + + @Path("/{cohortDefinitionId}/{sourceKey}") + @GET + public CohortSampleListDTO listCohortSamples( + @PathParam("cohortDefinitionId") int cohortDefinitionId, + @PathParam("sourceKey") String sourceKey + ) { + Source source = getSource(sourceKey); + CohortSampleListDTO result = new CohortSampleListDTO(); + + result.setCohortDefinitionId(cohortDefinitionId); + result.setSourceId(source.getId()); + + CohortGenerationInfo generationInfo = generationInfoRepository.findOne( + new CohortGenerationInfoId(cohortDefinitionId, source.getId())); + result.setGenerationStatus(generationInfo != null ? generationInfo.getStatus() : null); + result.setIsValid(generationInfo != null && generationInfo.isIsValid()); + + result.setSamples(this.samplingService.listSamples(cohortDefinitionId, source.getId())); + + return result; + } + + @Path("/{cohortDefinitionId}/{sourceKey}/{sampleId}") + @GET + public CohortSampleDTO getCohortSample( + @PathParam("sampleId") Integer sampleId, + @DefaultValue("") @QueryParam("fields") String fields + ) { + List returnFields = Arrays.asList(fields.split(",")); + boolean withRecordCounts = returnFields.contains("recordCount"); + return this.samplingService.getSample(sampleId, withRecordCounts); + } + + @Path("/{cohortDefinitionId}/{sourceKey}/{sampleId}/refresh") + @POST + public CohortSampleDTO refreshCohortSample( + @PathParam("sampleId") Integer sampleId, + @DefaultValue("") @QueryParam("fields") String fields + ) { + List returnFields = Arrays.asList(fields.split(",")); + boolean withRecordCounts = returnFields.contains("recordCount"); + this.samplingService.refreshSample(sampleId); + return this.samplingService.getSample(sampleId, withRecordCounts); + } + + @Path("/has-samples/{cohortDefinitionId}") + @GET + public Map hasSamples( + @PathParam("cohortDefinitionId") int cohortDefinitionId + ) { + int nSamples = this.samplingService.countSamples(cohortDefinitionId); + return Collections.singletonMap("hasSamples", nSamples > 0); + } + + @Path("/has-samples/{cohortDefinitionId}/{sourceKey}") + @GET + public Map hasSamples( + @PathParam("sourceKey") String sourceKey, + @PathParam("cohortDefinitionId") int cohortDefinitionId + ) { + Source source = getSource(sourceKey); + int nSamples = this.samplingService.countSamples(cohortDefinitionId, source.getId()); + return Collections.singletonMap("hasSamples", nSamples > 0); + } + + @Path("/{cohortDefinitionId}/{sourceKey}") + @POST + @Consumes(MediaType.APPLICATION_JSON) + public CohortSampleDTO createCohortSample( + @PathParam("sourceKey") String sourceKey, + @PathParam("cohortDefinitionId") int cohortDefinitionId, + SampleParametersDTO sampleParameters + ) { + sampleParameters.validate(); + Source source = getSource(sourceKey); + if (cohortDefinitionRepository.findOne(cohortDefinitionId) == null) { + throw new NotFoundException("Cohort definition " + cohortDefinitionId + " does not exist."); + } + CohortGenerationInfo generationInfo = generationInfoRepository.findOne( + new CohortGenerationInfoId(cohortDefinitionId, source.getId())); + if (generationInfo == null || generationInfo.getStatus() != GenerationStatus.COMPLETE) { + throw new BadRequestException("Cohort is not yet generated"); + } + return samplingService.createSample(source, cohortDefinitionId, sampleParameters); + } + + @Path("/{cohortDefinitionId}/{sourceKey}/{sampleId}") + @DELETE + public Response deleteCohortSample( + @PathParam("sourceKey") String sourceKey, + @PathParam("cohortDefinitionId") int cohortDefinitionId, + @PathParam("sampleId") int sampleId + ) { + Source source = getSource(sourceKey); + if (cohortDefinitionRepository.findOne(cohortDefinitionId) == null) { + throw new NotFoundException("Cohort definition " + cohortDefinitionId + " does not exist."); + } + samplingService.deleteSample(cohortDefinitionId, source, sampleId); + return Response.status(Response.Status.NO_CONTENT).build(); + } + + @Path("/{cohortDefinitionId}/{sourceKey}") + @DELETE + public Response deleteCohortSamples( + @PathParam("sourceKey") String sourceKey, + @PathParam("cohortDefinitionId") int cohortDefinitionId + ) { + Source source = getSource(sourceKey); + if (cohortDefinitionRepository.findOne(cohortDefinitionId) == null) { + throw new NotFoundException("Cohort definition " + cohortDefinitionId + " does not exist."); + } + samplingService.launchDeleteSamplesTasklet(cohortDefinitionId, source.getId()); + return Response.status(Response.Status.ACCEPTED).build(); + } + + private Source getSource(String sourceKey) { + Source source = sourceRepository.findBySourceKey(sourceKey); + if (source == null) { + throw new NotFoundException("Source " + sourceKey + " does not exist"); + } + return source; + } +} diff --git a/src/main/java/org/ohdsi/webapi/service/DDLService.java b/src/main/java/org/ohdsi/webapi/service/DDLService.java index b46de8a596..5d636e04a8 100644 --- a/src/main/java/org/ohdsi/webapi/service/DDLService.java +++ b/src/main/java/org/ohdsi/webapi/service/DDLService.java @@ -72,6 +72,8 @@ public class DDLService { "/ddl/results/heracles_results.sql", "/ddl/results/heracles_results_dist.sql", "/ddl/results/heracles_periods.sql", + // cohort sampling + "/ddl/results/cohort_sample_element.sql", // incidence rates "/ddl/results/ir_analysis_dist.sql", "/ddl/results/ir_analysis_result.sql", diff --git a/src/main/java/org/ohdsi/webapi/util/PreparedStatementRenderer.java b/src/main/java/org/ohdsi/webapi/util/PreparedStatementRenderer.java index b69d021bbd..2bc30fdcaf 100644 --- a/src/main/java/org/ohdsi/webapi/util/PreparedStatementRenderer.java +++ b/src/main/java/org/ohdsi/webapi/util/PreparedStatementRenderer.java @@ -293,18 +293,18 @@ public PreparedStatementSetter getSetter() { private Object convertPrimitiveArraysToWrapperArrays(Object value) { - if (value == null) return value; + if (value == null) return null; if (!value.getClass().isArray()) return value; if (value instanceof boolean[]) return ArrayUtils.toObject((boolean[]) value); if (value instanceof byte[]) return ArrayUtils.toObject((byte[]) value); if (value instanceof char[]) return ArrayUtils.toObject((char[]) value); - if (value instanceof char[]) return ArrayUtils.toObject((double[]) value); - if (value instanceof char[]) return ArrayUtils.toObject((float[]) value); - if (value instanceof char[]) return ArrayUtils.toObject((int[]) value); + if (value instanceof double[]) return ArrayUtils.toObject((double[]) value); + if (value instanceof float[]) return ArrayUtils.toObject((float[]) value); + if (value instanceof int[]) return ArrayUtils.toObject((int[]) value); if (value instanceof long[]) return ArrayUtils.toObject((long[]) value); - if (value instanceof char[]) return ArrayUtils.toObject((short[]) value); + if (value instanceof short[]) return ArrayUtils.toObject((short[]) value); return value; } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index c851eff3e3..c278698048 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -4,9 +4,13 @@ build.number=NA spring.profiles.active=${spring.profiles.active} # Logging -logging.level.org.springframework.web: OFF -logging.level.org.hibernate: OFF -logging.level.org.apache.shiro=TRACE +logging.level.org.springframework.web=${logging.level.org.springframework.web} +logging.level.org.hibernate=${logging.level.org.hibernate} +logging.level.root=${logging.level.root} +logging.level.org.ohdsi=${logging.level.org.ohdsi} +logging.level.org.springframework.orm=${logging.level.org.springframework.orm} +logging.level.org.springframework.jdbc=${logging.level.org.springframework.jdbc} +logging.level.org.apache.shiro=${logging.level.org.apache.shiro} #Primary DataSource datasource.driverClassName=${datasource.driverClassName} diff --git a/src/main/resources/db/migration/oracle/V2.8.0.20200109100200__cohort_sample_tables.sql b/src/main/resources/db/migration/oracle/V2.8.0.20200109100200__cohort_sample_tables.sql new file mode 100644 index 0000000000..dbc2915aff --- /dev/null +++ b/src/main/resources/db/migration/oracle/V2.8.0.20200109100200__cohort_sample_tables.sql @@ -0,0 +1,23 @@ +CREATE SEQUENCE ${ohdsiSchema}.cohort_sample_sequence; + +CREATE TABLE ${ohdsiSchema}.cohort_sample( + id NUMBER(10) PRIMARY KEY, + name VARCHAR(255) NOT NULL, + cohort_definition_id NUMBER(10) NOT NULL, + source_id NUMBER(10) NOT NULL, + size NUMBER(9) NOT NULL, + age_min NUMBER(4), + age_max NUMBER(4), + age_mode VARCHAR(24), + gender_concept_ids VARCHAR(255) NULL, + created_by_id INTEGER, + created_date TIMESTAMP WITH TIME ZONE DEFAULT sysdate NOT NULL, + modified_by_id INTEGER, + modified_date TIMESTAMP WITH TIME ZONE, + CONSTRAINT fk_cohort_sample_definition_id FOREIGN KEY (cohort_definition_id) + REFERENCES ${ohdsiSchema}.cohort_definition (id) ON DELETE CASCADE, + CONSTRAINT fk_cohort_sample_source_id FOREIGN KEY (source_id) + REFERENCES ${ohdsiSchema}.source (source_id) ON DELETE CASCADE +); + +CREATE INDEX idx_cohort_sample_source ON ${ohdsiSchema}.cohort_sample (cohort_definition_id, source_id); diff --git a/src/main/resources/db/migration/oracle/V2.8.0.20200122173000__insert_cohort_sample_permissions.sql b/src/main/resources/db/migration/oracle/V2.8.0.20200122173000__insert_cohort_sample_permissions.sql new file mode 100644 index 0000000000..e019c66c3d --- /dev/null +++ b/src/main/resources/db/migration/oracle/V2.8.0.20200122173000__insert_cohort_sample_permissions.sql @@ -0,0 +1,19 @@ +INSERT INTO ${ohdsiSchema}.sec_permission(id, value, description) VALUES +(${ohdsiSchema}.sec_permission_id_seq.nextval, 'cohortsample:*:*:get', 'List cohort samples'), +(${ohdsiSchema}.sec_permission_id_seq.nextval, 'cohortsample:*:*:*:get', 'Get single cohort samples'), +(${ohdsiSchema}.sec_permission_id_seq.nextval, 'cohortsample:*:*:*:delete', 'Delete cohort sample'), +(${ohdsiSchema}.sec_permission_id_seq.nextval, 'cohortsample:*:*:delete', 'Delete all cohort samples of a cohort.'), +(${ohdsiSchema}.sec_permission_id_seq.nextval, 'cohortsample:*:*:post', 'Create cohort sample') +(${ohdsiSchema}.sec_permission_id_seq.nextval, 'cohortsample:*:*:*:refresh:post', 'Refresh cohort sample'); + +INSERT INTO ${ohdsiSchema}.sec_role_permission(id, role_id, permission_id) +SELECT NEXT VALUE FOR ${ohdsiSchema}.sec_role_permission_sequence, sr.id, sp.id +FROM ${ohdsiSchema}.sec_permission SP, ${ohdsiSchema}.sec_role sr +WHERE sp.value IN ( + 'cohortsample:*:*:get', + 'cohortsample:*:*:*:get', + 'cohortsample:*:*:*:delete', + 'cohortsample:*:*:delete', + 'cohortsample:*:*:post', + 'cohortsample:*:*:*:refresh:post' + ) AND sr.name IN ('Atlas users'); diff --git a/src/main/resources/db/migration/postgresql/V2.8.0.20200109100200__cohort_sample_tables.sql b/src/main/resources/db/migration/postgresql/V2.8.0.20200109100200__cohort_sample_tables.sql new file mode 100644 index 0000000000..ebedb5cb72 --- /dev/null +++ b/src/main/resources/db/migration/postgresql/V2.8.0.20200109100200__cohort_sample_tables.sql @@ -0,0 +1,23 @@ +CREATE SEQUENCE ${ohdsiSchema}.cohort_sample_sequence; + +CREATE TABLE ${ohdsiSchema}.cohort_sample( + id INTEGER PRIMARY KEY NOT NULL, + name VARCHAR(255) NOT NULL, + cohort_definition_id INTEGER NOT NULL, + source_id INTEGER NOT NULL, + size INTEGER NOT NULL, + age_min SMALLINT NULL, + age_max SMALLINT NULL, + age_mode VARCHAR(24), + gender_concept_ids VARCHAR(255) NULL, + created_by_id INTEGER, + created_date TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT (now()), + modified_by_id INTEGER, + modified_date TIMESTAMP WITH TIME ZONE, + CONSTRAINT fk_cohort_sample_definition_id FOREIGN KEY (cohort_definition_id) + REFERENCES ${ohdsiSchema}.cohort_definition (id) ON DELETE CASCADE, + CONSTRAINT fk_cohort_sample_source_id FOREIGN KEY (source_id) + REFERENCES ${ohdsiSchema}.source (source_id) ON DELETE CASCADE +); + +CREATE INDEX idx_cohort_sample_source ON ${ohdsiSchema}.cohort_sample (cohort_definition_id, source_id); diff --git a/src/main/resources/db/migration/postgresql/V2.8.0.20200122173000__insert_cohort_sample_permissions.sql b/src/main/resources/db/migration/postgresql/V2.8.0.20200122173000__insert_cohort_sample_permissions.sql new file mode 100644 index 0000000000..27a2bc586b --- /dev/null +++ b/src/main/resources/db/migration/postgresql/V2.8.0.20200122173000__insert_cohort_sample_permissions.sql @@ -0,0 +1,19 @@ +INSERT INTO ${ohdsiSchema}.sec_permission(id, value, description) VALUES +(nextval('${ohdsiSchema}.sec_permission_id_seq'), 'cohortsample:*:*:get', 'List cohort samples'), +(nextval('${ohdsiSchema}.sec_permission_id_seq'), 'cohortsample:*:*:*:get', 'Get single cohort samples'), +(nextval('${ohdsiSchema}.sec_permission_id_seq'), 'cohortsample:*:*:*:delete', 'Delete cohort sample'), +(nextval('${ohdsiSchema}.sec_permission_id_seq'), 'cohortsample:*:*:delete', 'Delete all cohort samples of a cohort.'), +(nextval('${ohdsiSchema}.sec_permission_id_seq'), 'cohortsample:*:*:post', 'Create cohort sample'), +(nextval('${ohdsiSchema}.sec_permission_id_seq'), 'cohortsample:*:*:*:refresh:post', 'Refresh cohort sample'); + +INSERT INTO ${ohdsiSchema}.sec_role_permission(id, role_id, permission_id) +SELECT nextval('${ohdsiSchema}.sec_role_permission_sequence'), sr.id, sp.id +FROM ${ohdsiSchema}.sec_permission SP, ${ohdsiSchema}.sec_role sr +WHERE sp.value IN ( + 'cohortsample:*:*:get', + 'cohortsample:*:*:*:get', + 'cohortsample:*:*:*:delete', + 'cohortsample:*:*:delete', + 'cohortsample:*:*:post', + 'cohortsample:*:*:*:refresh:post' + ) AND sr.name IN ('Atlas users'); diff --git a/src/main/resources/db/migration/sqlserver/V2.8.0.20200109100200__cohort_sample_tables.sql b/src/main/resources/db/migration/sqlserver/V2.8.0.20200109100200__cohort_sample_tables.sql new file mode 100644 index 0000000000..927913b4da --- /dev/null +++ b/src/main/resources/db/migration/sqlserver/V2.8.0.20200109100200__cohort_sample_tables.sql @@ -0,0 +1,23 @@ +CREATE SEQUENCE ${ohdsiSchema}.cohort_sample_sequence; + +CREATE TABLE ${ohdsiSchema}.cohort_sample( + id BIGINT PRIMARY KEY CLUSTERED NOT NULL, + name VARCHAR(255) NOT NULL, + cohort_definition_id INTEGER NOT NULL, + source_id INTEGER NOT NULL, + size INT NOT NULL, + age_min SMALLINT NULL, + age_max SMALLINT NULL, + age_mode VARCHAR(24), + gender_concept_ids VARCHAR(255) NULL, + created_by_id INTEGER, + created_date DATETIMEOFFSET, + modified_by_id INTEGER, + modified_date DATETIMEOFFSET, + CONSTRAINT fk_cohort_sample_definition_id FOREIGN KEY (cohort_definition_id) + REFERENCES ${ohdsiSchema}.cohort_definition (id) ON DELETE CASCADE, + CONSTRAINT fk_cohort_sample_source_id FOREIGN KEY (source_id) + REFERENCES ${ohdsiSchema}.source (source_id) ON DELETE CASCADE +); + +CREATE INDEX idx_cohort_sample_source ON ${ohdsiSchema}.cohort_sample (cohort_definition_id, source_id); diff --git a/src/main/resources/db/migration/sqlserver/V2.8.0.20200122173000__insert_cohort_sample_permissions.sql b/src/main/resources/db/migration/sqlserver/V2.8.0.20200122173000__insert_cohort_sample_permissions.sql new file mode 100644 index 0000000000..73751c94a1 --- /dev/null +++ b/src/main/resources/db/migration/sqlserver/V2.8.0.20200122173000__insert_cohort_sample_permissions.sql @@ -0,0 +1,19 @@ +INSERT INTO ${ohdsiSchema}.sec_permission(id, value, description) VALUES +(NEXT VALUE FOR ${ohdsiSchema}.sec_permission_id_seq, 'cohortsample:*:*:get', 'List cohort samples'), +(NEXT VALUE FOR ${ohdsiSchema}.sec_permission_id_seq, 'cohortsample:*:*:*:get', 'Get single cohort samples'), +(NEXT VALUE FOR ${ohdsiSchema}.sec_permission_id_seq, 'cohortsample:*:*:*:delete', 'Delete cohort sample'), +(NEXT VALUE FOR ${ohdsiSchema}.sec_permission_id_seq, 'cohortsample:*:*:delete', 'Delete all cohort samples of a cohort.'), +(NEXT VALUE FOR ${ohdsiSchema}.sec_permission_id_seq, 'cohortsample:*:*:post', 'Create cohort sample'), +(NEXT VALUE FOR ${ohdsiSchema}.sec_permission_id_seq, 'cohortsample:*:*:*:refresh:post', 'Refresh cohort sample'); + +INSERT INTO ${ohdsiSchema}.sec_role_permission(id, role_id, permission_id) +SELECT NEXT VALUE FOR ${ohdsiSchema}.sec_role_permission_sequence, sr.id, sp.id +FROM ${ohdsiSchema}.sec_permission SP, ${ohdsiSchema}.sec_role sr +WHERE sp.value IN ( + 'cohortsample:*:*:get', + 'cohortsample:*:*:*:get', + 'cohortsample:*:*:*:delete', + 'cohortsample:*:*:delete', + 'cohortsample:*:*:post', + 'cohortsample:*:*:*:refresh:post' + ) AND sr.name IN ('Atlas users'); diff --git a/src/main/resources/ddl/results/cohort_sample_element.sql b/src/main/resources/ddl/results/cohort_sample_element.sql new file mode 100644 index 0000000000..568c057214 --- /dev/null +++ b/src/main/resources/ddl/results/cohort_sample_element.sql @@ -0,0 +1,8 @@ +IF OBJECT_ID('@results_schema.cohort_sample_element', 'U') IS NULL +CREATE TABLE @results_schema.cohort_sample_element( + cohort_sample_id int NOT NULL, + rank_value int NOT NULL, + person_id bigint NOT NULL, + age int, + gender_concept_id int +); diff --git a/src/main/resources/ddl/results/create_index.sql b/src/main/resources/ddl/results/create_index.sql index 6dc2ab8fee..8fff880be7 100644 --- a/src/main/resources/ddl/results/create_index.sql +++ b/src/main/resources/ddl/results/create_index.sql @@ -10,4 +10,6 @@ CREATE INDEX HR_IDX_COHORT_ID_FIRST_RES ON @results_schema.HERACLES_RESULTS (coh CREATE INDEX HH_IDX_COHORT_ID_ANALYSIS_ID ON @results_schema.HERACLES_HEEL_RESULTS (cohort_definition_id, analysis_id); CREATE INDEX idx_heracles_periods_startdate ON @results_schema.heracles_periods (period_start_date); -CREATE INDEX idx_heracles_periods_end_date ON @results_schema.heracles_periods (period_end_date); \ No newline at end of file +CREATE INDEX idx_heracles_periods_end_date ON @results_schema.heracles_periods (period_end_date); + +CREATE INDEX idx_cohort_sample_element_rank ON @results_schema.cohort_sample_element (cohort_sample_id, rank); \ No newline at end of file diff --git a/src/main/resources/resources/cohortsample/sql/deleteSampleElementsById.sql b/src/main/resources/resources/cohortsample/sql/deleteSampleElementsById.sql new file mode 100644 index 0000000000..bf6e68560c --- /dev/null +++ b/src/main/resources/resources/cohortsample/sql/deleteSampleElementsById.sql @@ -0,0 +1,3 @@ +DELETE FROM @results_schema.cohort_sample_element +WHERE cohort_sample_id IN ( @cohortSampleId ) +; \ No newline at end of file diff --git a/src/main/resources/resources/cohortsample/sql/findElementsByCohortSampleId.sql b/src/main/resources/resources/cohortsample/sql/findElementsByCohortSampleId.sql new file mode 100644 index 0000000000..04d35949d9 --- /dev/null +++ b/src/main/resources/resources/cohortsample/sql/findElementsByCohortSampleId.sql @@ -0,0 +1,5 @@ +SELECT * +FROM @results_schema.cohort_sample_element s +WHERE s.cohort_sample_id = @cohortSampleId +ORDER BY s.rank_value +; \ No newline at end of file diff --git a/src/main/resources/resources/cohortsample/sql/findElementsByCohortSampleIdWithCounts.sql b/src/main/resources/resources/cohortsample/sql/findElementsByCohortSampleIdWithCounts.sql new file mode 100644 index 0000000000..f1cb7b4e87 --- /dev/null +++ b/src/main/resources/resources/cohortsample/sql/findElementsByCohortSampleIdWithCounts.sql @@ -0,0 +1,44 @@ +SELECT *, + ((select COUNT(*) + from @CDM_schema.drug_exposure c + where c.person_id = s.person_id) + + + (select COUNT(*) + from @CDM_schema.condition_occurrence c + where c.person_id = s.person_id) + + + (select COUNT(*) + from @CDM_schema.condition_era c + where c.person_id = s.person_id) + + + (select COUNT(*) + from @CDM_schema.observation c + where c.person_id = s.person_id) + + + (select COUNT(*) + from @CDM_schema.visit_occurrence c + where c.person_id = s.person_id) + + + (select COUNT(*) + from @CDM_schema.death c + where c.person_id = s.person_id) + + + (select COUNT(*) + from @CDM_schema.measurement c + where c.person_id = s.person_id) + + + (select COUNT(*) + from @CDM_schema.device_exposure c + where c.person_id = s.person_id) + + + (select COUNT(*) + from @CDM_schema.procedure_occurrence c + where c.person_id = s.person_id) + + + (select COUNT(*) + from @CDM_schema.specimen c + where c.person_id = s.person_id)) as record_count +FROM @results_schema.cohort_sample_element s +WHERE s.cohort_sample_id = @cohortSampleId +ORDER BY s.rank_value +; \ No newline at end of file diff --git a/src/main/resources/resources/cohortsample/sql/generateSample.sql b/src/main/resources/resources/cohortsample/sql/generateSample.sql new file mode 100644 index 0000000000..309e36cbac --- /dev/null +++ b/src/main/resources/resources/cohortsample/sql/generateSample.sql @@ -0,0 +1,14 @@ +SELECT * +FROM ( + SELECT + cohort.subject_id AS person_id, + cohort.cohort_definition_id AS cohort_definition_id, + person.gender_concept_id AS gender_concept_id, + CAST(year(cohort.cohort_start_date) - person.year_of_birth AS INT) AS age + FROM @results_schema.cohort + JOIN @CDM_schema.person ON person_id = subject_id +) AS cte +WHERE cohort_definition_id = @cohort_definition_id + @expression +ORDER BY RAND() +; \ No newline at end of file diff --git a/src/main/resources/resources/cohortsample/sql/insertSampleElement.sql b/src/main/resources/resources/cohortsample/sql/insertSampleElement.sql new file mode 100644 index 0000000000..ddaaff250e --- /dev/null +++ b/src/main/resources/resources/cohortsample/sql/insertSampleElement.sql @@ -0,0 +1,3 @@ +INSERT INTO @results_schema.cohort_sample_element (cohort_sample_id, rank_value, person_id, age, gender_concept_id) +VALUES (@cohortSampleId, @rank, @personId, @age, @genderConceptId) +;