From 3640e5d6b221ea64c296f1deb581d6b247718fbf Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Thu, 7 May 2026 15:01:45 -0400 Subject: [PATCH 01/26] Add new filter/sort models --- src/main/java/io/swagger/model/FilterBy.java | 25 +++++++++++++++ .../java/io/swagger/model/SearchRequest.java | 7 +++++ .../java/io/swagger/model/core/SortBy.java | 2 ++ .../model/core/StudySearchRequest.java | 2 ++ .../model/core/TrialSearchRequest.java | 23 +------------- .../java/io/swagger/model/sort/SortBy.java | 31 +++++++++++++++++++ .../model/{core => sort}/SortOrder.java | 2 +- .../service/SearchQueryBuilder.java | 2 +- .../service/core/StudyService.java | 3 +- .../service/core/TrialService.java | 1 + 10 files changed, 72 insertions(+), 26 deletions(-) create mode 100644 src/main/java/io/swagger/model/FilterBy.java create mode 100644 src/main/java/io/swagger/model/sort/SortBy.java rename src/main/java/io/swagger/model/{core => sort}/SortOrder.java (95%) diff --git a/src/main/java/io/swagger/model/FilterBy.java b/src/main/java/io/swagger/model/FilterBy.java new file mode 100644 index 00000000..a34afb7d --- /dev/null +++ b/src/main/java/io/swagger/model/FilterBy.java @@ -0,0 +1,25 @@ +package io.swagger.model; + +import io.swagger.model.sort.SortOrder; + +public class FilterBy { + private String filterOn; + private boolean addInfoColumn = false; + + public String getFilterOn() { + return filterOn; + } + + public void setFilterOn(String filterOn) { + this.filterOn = filterOn; + } + + public boolean isAddInfoColumn() { + return addInfoColumn; + } + + public void setAddInfoColumn(boolean addInfoColumn) { + this.addInfoColumn = addInfoColumn; + } + +} diff --git a/src/main/java/io/swagger/model/SearchRequest.java b/src/main/java/io/swagger/model/SearchRequest.java index 8ad217c4..ae723e1c 100644 --- a/src/main/java/io/swagger/model/SearchRequest.java +++ b/src/main/java/io/swagger/model/SearchRequest.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.model.sort.SortBy; import java.util.ArrayList; import java.util.List; @@ -25,6 +26,12 @@ public abstract class SearchRequest { @JsonProperty("externalReferenceSources") protected List externalReferenceSources = null; + @JsonProperty("filterBy") + protected FilterBy filterBy = null; + + @JsonProperty("sortBy") + protected List sortBy = null; + final public SearchRequest page(Integer page) { this.page = page; return this; diff --git a/src/main/java/io/swagger/model/core/SortBy.java b/src/main/java/io/swagger/model/core/SortBy.java index 8a132a3e..bf215cc5 100644 --- a/src/main/java/io/swagger/model/core/SortBy.java +++ b/src/main/java/io/swagger/model/core/SortBy.java @@ -3,6 +3,8 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; +@Deprecated +// TODO: Phase out in favor of io.swagger.model.sort objects public enum SortBy { STUDYDBID("studyDbId"), diff --git a/src/main/java/io/swagger/model/core/StudySearchRequest.java b/src/main/java/io/swagger/model/core/StudySearchRequest.java index 0b26436b..26849475 100644 --- a/src/main/java/io/swagger/model/core/StudySearchRequest.java +++ b/src/main/java/io/swagger/model/core/StudySearchRequest.java @@ -5,6 +5,8 @@ import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.model.SearchRequest; +import io.swagger.model.sort.SortOrder; + import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/io/swagger/model/core/TrialSearchRequest.java b/src/main/java/io/swagger/model/core/TrialSearchRequest.java index 830dde51..5a4b741f 100644 --- a/src/main/java/io/swagger/model/core/TrialSearchRequest.java +++ b/src/main/java/io/swagger/model/core/TrialSearchRequest.java @@ -3,6 +3,7 @@ import java.util.Objects; import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.model.SearchRequest; + import java.util.ArrayList; import java.util.List; import java.time.LocalDate; @@ -50,28 +51,6 @@ public class TrialSearchRequest extends SearchRequest { @JsonProperty("trialPUIs") private List trialPUIs = null; - @JsonProperty("sortBy") - private SortBy sortBy = null; - - @JsonProperty("sortOrder") - private SortOrder sortOrder = null; - - public SortBy getSortBy() { - return sortBy; - } - - public void setSortBy(SortBy sortBy) { - this.sortBy = sortBy; - } - - public SortOrder getSortOrder() { - return sortOrder; - } - - public void setSortOrder(SortOrder sortOrder) { - this.sortOrder = sortOrder; - } - public TrialSearchRequest commonCropNames(List commonCropNames) { this.commonCropNames = commonCropNames; return this; diff --git a/src/main/java/io/swagger/model/sort/SortBy.java b/src/main/java/io/swagger/model/sort/SortBy.java new file mode 100644 index 00000000..8501bd88 --- /dev/null +++ b/src/main/java/io/swagger/model/sort/SortBy.java @@ -0,0 +1,31 @@ +package io.swagger.model.sort; + +public class SortBy { + private String sortedOn; + private SortOrder sortOrder = SortOrder.ASC; + private boolean addInfoColumn = false; + + public String getSortedOn() { + return sortedOn; + } + + public void setSortedOn(String sortedOn) { + this.sortedOn = sortedOn; + } + + public SortOrder getSortOrder() { + return sortOrder; + } + + public void setSortOrder(SortOrder sortOrder) { + this.sortOrder = sortOrder; + } + + public boolean isAddInfoColumn() { + return addInfoColumn; + } + + public void setAddInfoColumn(boolean addInfoColumn) { + this.addInfoColumn = addInfoColumn; + } +} diff --git a/src/main/java/io/swagger/model/core/SortOrder.java b/src/main/java/io/swagger/model/sort/SortOrder.java similarity index 95% rename from src/main/java/io/swagger/model/core/SortOrder.java rename to src/main/java/io/swagger/model/sort/SortOrder.java index 0e5b36bd..4cc9d5aa 100644 --- a/src/main/java/io/swagger/model/core/SortOrder.java +++ b/src/main/java/io/swagger/model/sort/SortOrder.java @@ -1,4 +1,4 @@ -package io.swagger.model.core; +package io.swagger.model.sort; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java index 80bf5bc5..74ef4677 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -7,7 +7,7 @@ import java.time.OffsetDateTime; import io.swagger.model.GeoJSONSearchArea; -import io.swagger.model.core.SortOrder; +import io.swagger.model.sort.SortOrder; public class SearchQueryBuilder { diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/core/StudyService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/core/StudyService.java index 1b6526ea..4c0cb198 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/core/StudyService.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/core/StudyService.java @@ -6,7 +6,6 @@ import org.apache.commons.lang3.StringUtils; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerDbIdNotFoundException; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; -import org.brapi.test.BrAPITestServer.model.entity.BrAPIBaseEntity; import org.brapi.test.BrAPITestServer.model.entity.core.CropEntity; import org.brapi.test.BrAPITestServer.model.entity.core.DataLinkEntity; import org.brapi.test.BrAPITestServer.model.entity.core.EnvironmentParametersEntity; @@ -39,7 +38,7 @@ import io.swagger.model.core.Contact; import io.swagger.model.core.EnvironmentParameter; import io.swagger.model.core.SortBy; -import io.swagger.model.core.SortOrder; +import io.swagger.model.sort.SortOrder; import io.swagger.model.core.Study; import io.swagger.model.core.StudyExperimentalDesign; import io.swagger.model.core.StudyGrowthFacility; diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/core/TrialService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/core/TrialService.java index fb798a5b..36ac0b0a 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/core/TrialService.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/core/TrialService.java @@ -4,6 +4,7 @@ import java.util.stream.Collectors; import io.swagger.model.core.*; +import io.swagger.model.sort.SortOrder; import jakarta.validation.Valid; import org.brapi.test.BrAPITestServer.exceptions.BatchDeleteWrongTypeException; From fd641789e287f6d39342d8d4763db452d6117f01 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Thu, 7 May 2026 18:57:31 -0400 Subject: [PATCH 02/26] Preliminary generic sortBy implementation on searchRequests --- src/main/java/io/swagger/model/FilterBy.java | 21 ++++-- .../java/io/swagger/model/SearchRequest.java | 20 +++++- .../sort/{SortBy.java => SortByEntry.java} | 11 +++- .../service/SearchQueryBuilder.java | 64 ++++++++++++++++++- .../service/core/TrialService.java | 42 ++---------- 5 files changed, 111 insertions(+), 47 deletions(-) rename src/main/java/io/swagger/model/sort/{SortBy.java => SortByEntry.java} (63%) diff --git a/src/main/java/io/swagger/model/FilterBy.java b/src/main/java/io/swagger/model/FilterBy.java index a34afb7d..6d72b821 100644 --- a/src/main/java/io/swagger/model/FilterBy.java +++ b/src/main/java/io/swagger/model/FilterBy.java @@ -1,17 +1,24 @@ package io.swagger.model; -import io.swagger.model.sort.SortOrder; - public class FilterBy { - private String filterOn; + private String filterColumn; + private String value; private boolean addInfoColumn = false; - public String getFilterOn() { - return filterOn; + public String getFilterColumn() { + return filterColumn; + } + + public void setFilterColumn(String filterColumn) { + this.filterColumn = filterColumn; + } + + public String getValue() { + return value; } - public void setFilterOn(String filterOn) { - this.filterOn = filterOn; + public void setValue(String value) { + this.value = value; } public boolean isAddInfoColumn() { diff --git a/src/main/java/io/swagger/model/SearchRequest.java b/src/main/java/io/swagger/model/SearchRequest.java index ae723e1c..6d999d8a 100644 --- a/src/main/java/io/swagger/model/SearchRequest.java +++ b/src/main/java/io/swagger/model/SearchRequest.java @@ -2,7 +2,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; -import io.swagger.model.sort.SortBy; +import io.swagger.model.sort.SortByEntry; import java.util.ArrayList; import java.util.List; @@ -30,7 +30,7 @@ public abstract class SearchRequest { protected FilterBy filterBy = null; @JsonProperty("sortBy") - protected List sortBy = null; + protected List sortBy = null; final public SearchRequest page(Integer page) { this.page = page; @@ -126,4 +126,20 @@ public void addExternalReferenceItem(String externalReferenceId, String external } } + + public FilterBy getFilterBy() { + return filterBy; + } + + public void setFilterBy(FilterBy filterBy) { + this.filterBy = filterBy; + } + + public List getSortByEntry() { + return sortBy; + } + + public void setSortByEntry(List sortBy) { + this.sortBy = sortBy; + } } diff --git a/src/main/java/io/swagger/model/sort/SortBy.java b/src/main/java/io/swagger/model/sort/SortByEntry.java similarity index 63% rename from src/main/java/io/swagger/model/sort/SortBy.java rename to src/main/java/io/swagger/model/sort/SortByEntry.java index 8501bd88..3d7fee7f 100644 --- a/src/main/java/io/swagger/model/sort/SortBy.java +++ b/src/main/java/io/swagger/model/sort/SortByEntry.java @@ -1,10 +1,19 @@ package io.swagger.model.sort; -public class SortBy { +// TODO: Replace io.swagger.model.core.SortBy with this class and rename this class to SortBy +public class SortByEntry { private String sortedOn; private SortOrder sortOrder = SortOrder.ASC; private boolean addInfoColumn = false; + public SortByEntry(String sortedOn, + SortOrder sortOrder, + boolean addInfoColumn) { + this.sortedOn = sortedOn; + this.sortOrder = sortOrder; + this.addInfoColumn = addInfoColumn; + } + public String getSortedOn() { return sortedOn; } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java index 74ef4677..67ab3378 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -6,7 +6,9 @@ import java.time.LocalDate; import java.time.OffsetDateTime; +import io.swagger.model.FilterBy; import io.swagger.model.GeoJSONSearchArea; +import io.swagger.model.sort.SortByEntry; import io.swagger.model.sort.SortOrder; public class SearchQueryBuilder { @@ -311,6 +313,8 @@ private String paramFilter(String param) { return param.replace('.', '_').replace('*', '_'); } + @Deprecated + // Use withSortBy instead public SearchQueryBuilder withSort(String sortByStr, SortOrder sortOrder) { String sortOrderStr = "ASC"; if (sortOrder != null) { @@ -321,4 +325,62 @@ public SearchQueryBuilder withSort(String sortByStr, SortOrder sortOrder) { return this; } -} + + /** + * Takes a list of SortBy options that should typically come in a searchRequest. + * Applies the entries in the list to sort the SearchQuery. + * + * A SortBy has + * - A column name + * - An order (DESC, ASC) + * - A boolean denoting whether the column to be sorted is data stored in additional info + */ + public SearchQueryBuilder sortBy(List sortBy) { + + if (sortBy == null || sortBy.isEmpty()) { + return this; + } + + for (SortByEntry sort : sortBy) { + if (sortBy.getFirst().equals(sort)) { + this.sortClause += " ORDER BY "; + buildSort(sort); + } + + this.sortClause += ", "; + buildSort(sort); + } + + return this; + } + + private void buildSort(SortByEntry sort) { + if (sort.isAddInfoColumn()) { + // TODO: This assumes the jsonb value of the key is always text. Might need to support numerical sort. + this.sortClause += "additional_info ->>" + sort.getSortedOn() + " " + sort.getSortOrder() + " "; + } else { + this.sortClause += sort.getSortedOn() + " " + sort.getSortOrder() + " "; + } + } + + /** + * Takes a list of FilterBy options that should typically come in a searchRequest. + * Applies the entries in the list to filter the SearchQuery. + * + * A FilterBy has + * - A column name + * - A boolean denoting whether the column to be sorted is data stored in additional info + */ + public SearchQueryBuilder filterBy(List filterBy) { + for (FilterBy filter : filterBy) { + if (filter.isAddInfoColumn()) { + // TODO: This assumes the jsonb value of the key is always text. Might need to support numerical sort. + this.whereClause += " AND additional_info ->> " + filter.getFilterColumn() + " = " + filter.getValue() + " "; + } + else { + this.whereClause += " AND additional_info ->> " + filter.getFilterColumn() + " " + filter.getValue() + " "; + } + } + + return this; + }} diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/core/TrialService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/core/TrialService.java index 36ac0b0a..a9ee35cc 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/core/TrialService.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/core/TrialService.java @@ -4,6 +4,7 @@ import java.util.stream.Collectors; import io.swagger.model.core.*; +import io.swagger.model.sort.SortByEntry; import io.swagger.model.sort.SortOrder; import jakarta.validation.Valid; @@ -95,11 +96,10 @@ public List findTrials(@Valid String commonCropName, @Valid String contac request.setSearchDateRangeStart(searchDateRangeStart); if (searchDateRangeEnd != null) request.setSearchDateRangeEnd(searchDateRangeEnd); - if (sortBy != null && SortBy.fromValue(sortBy) != null) - request.setSortBy(SortBy.fromValue(sortBy)); - if (sortOrder != null && SortOrder.fromValue(sortOrder) != null) - request.setSortOrder(SortOrder.fromValue(sortOrder)); - + if (sortBy != null) { + SortByEntry querySortBy = new SortByEntry(sortBy, SortOrder.valueOf(sortOrder), false); + request.setSortByEntry(List.of()); + } request.addExternalReferenceItem(externalReferenceId, externalReferenceID, externalReferenceSource); return findTrials(request, metadata); } @@ -127,7 +127,7 @@ public List findTrials(@Valid TrialSearchRequest request, Metadata metada .appendList(request.getStudyNames(), "*study.studyName").appendList(request.getTrialDbIds(), "id") .appendList(request.getTrialNames(), "trialName") .appendDateRange(request.getSearchDateRangeStart(), request.getSearchDateRangeEnd(), "startDate") - .withSort(getSortByField(request.getSortBy()), request.getSortOrder()); + .sortBy(request.getSortByEntry()); Page trialsPage = trialRepository.findAllBySearchAndPaginate(searchQuery, pageReq); PagingUtility.calculateMetaData(metadata, trialsPage); @@ -357,34 +357,4 @@ private PublicationEntity convertToEntity(TrialNewRequestPublications pub) { return entity; } - - private String getSortByField(SortBy sortBy) { - String sortByStr = "id"; - if (sortBy != null) { - switch (sortBy) { - case STARTDATE: - sortByStr = "startDate"; - break; - case ENDDATE: - sortByStr = "endDate"; - break; - case TRIALNAME: - sortByStr = "trialName"; - break; - case PROGRAMDBID: - sortByStr = "program.id"; - break; - case PROGRAMNAME: - sortByStr = "program.name"; - break; - case TRIALDBID: - default: - sortByStr = "id"; - break; - } - } - - return sortByStr; - } - } From 32664a2162b3757df800c9fd50ad7d8fb02b3ac8 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Thu, 21 May 2026 16:06:21 -0400 Subject: [PATCH 03/26] Use @Formula to derive needed addInfo entity attrs, update SearchQueryBuilder --- src/main/java/io/swagger/model/FilterBy.java | 9 ----- .../java/io/swagger/model/SearchRequest.java | 8 ++-- .../{SortByEntry.java => SortByElement.java} | 20 +++------- .../model/entity/core/TrialEntity.java | 8 ++++ .../service/SearchQueryBuilder.java | 37 ++++++++----------- .../service/core/TrialService.java | 6 +-- 6 files changed, 36 insertions(+), 52 deletions(-) rename src/main/java/io/swagger/model/sort/{SortByEntry.java => SortByElement.java} (58%) diff --git a/src/main/java/io/swagger/model/FilterBy.java b/src/main/java/io/swagger/model/FilterBy.java index 6d72b821..001ea05b 100644 --- a/src/main/java/io/swagger/model/FilterBy.java +++ b/src/main/java/io/swagger/model/FilterBy.java @@ -3,7 +3,6 @@ public class FilterBy { private String filterColumn; private String value; - private boolean addInfoColumn = false; public String getFilterColumn() { return filterColumn; @@ -21,12 +20,4 @@ public void setValue(String value) { this.value = value; } - public boolean isAddInfoColumn() { - return addInfoColumn; - } - - public void setAddInfoColumn(boolean addInfoColumn) { - this.addInfoColumn = addInfoColumn; - } - } diff --git a/src/main/java/io/swagger/model/SearchRequest.java b/src/main/java/io/swagger/model/SearchRequest.java index 6d999d8a..9d9695c8 100644 --- a/src/main/java/io/swagger/model/SearchRequest.java +++ b/src/main/java/io/swagger/model/SearchRequest.java @@ -2,7 +2,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; -import io.swagger.model.sort.SortByEntry; +import io.swagger.model.sort.SortByElement; import java.util.ArrayList; import java.util.List; @@ -30,7 +30,7 @@ public abstract class SearchRequest { protected FilterBy filterBy = null; @JsonProperty("sortBy") - protected List sortBy = null; + protected List sortBy = null; final public SearchRequest page(Integer page) { this.page = page; @@ -135,11 +135,11 @@ public void setFilterBy(FilterBy filterBy) { this.filterBy = filterBy; } - public List getSortByEntry() { + public List getSortByEntry() { return sortBy; } - public void setSortByEntry(List sortBy) { + public void setSortByEntry(List sortBy) { this.sortBy = sortBy; } } diff --git a/src/main/java/io/swagger/model/sort/SortByEntry.java b/src/main/java/io/swagger/model/sort/SortByElement.java similarity index 58% rename from src/main/java/io/swagger/model/sort/SortByEntry.java rename to src/main/java/io/swagger/model/sort/SortByElement.java index 3d7fee7f..cbc79600 100644 --- a/src/main/java/io/swagger/model/sort/SortByEntry.java +++ b/src/main/java/io/swagger/model/sort/SortByElement.java @@ -1,19 +1,19 @@ package io.swagger.model.sort; // TODO: Replace io.swagger.model.core.SortBy with this class and rename this class to SortBy -public class SortByEntry { +public class SortByElement { private String sortedOn; private SortOrder sortOrder = SortOrder.ASC; - private boolean addInfoColumn = false; - public SortByEntry(String sortedOn, - SortOrder sortOrder, - boolean addInfoColumn) { + public SortByElement(String sortedOn, + SortOrder sortOrder, + boolean addInfoColumn) { this.sortedOn = sortedOn; this.sortOrder = sortOrder; - this.addInfoColumn = addInfoColumn; } + public SortByElement() {} + public String getSortedOn() { return sortedOn; } @@ -29,12 +29,4 @@ public SortOrder getSortOrder() { public void setSortOrder(SortOrder sortOrder) { this.sortOrder = sortOrder; } - - public boolean isAddInfoColumn() { - return addInfoColumn; - } - - public void setAddInfoColumn(boolean addInfoColumn) { - this.addInfoColumn = addInfoColumn; - } } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/model/entity/core/TrialEntity.java b/src/main/java/org/brapi/test/BrAPITestServer/model/entity/core/TrialEntity.java index 1c9be57a..f18543fc 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/model/entity/core/TrialEntity.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/model/entity/core/TrialEntity.java @@ -4,8 +4,10 @@ import org.brapi.test.BrAPITestServer.model.entity.BrAPIPrimaryEntity; import org.brapi.test.BrAPITestServer.model.entity.pheno.ObservationEntity; import org.brapi.test.BrAPITestServer.model.entity.pheno.ObservationUnitEntity; +import org.hibernate.annotations.Formula; import org.hibernate.annotations.Where; +import java.time.OffsetDateTime; import java.util.Date; import java.util.List; @Entity @@ -38,6 +40,12 @@ public class TrialEntity extends BrAPIPrimaryEntity { @Column(name = "soft_deleted") private boolean softDeleted; + @Formula("(to_timestamp(additional_info #>> '{createdDate}', 'YYYY-MM-DD'))") + private OffsetDateTime createdDate; + + @Formula("(additional_info #>> '{createdBy,userName}')") + private String createdBy; + @ManyToOne(fetch = FetchType.LAZY) private CropEntity crop; @ManyToOne(fetch = FetchType.LAZY) diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java index 67ab3378..41a423ec 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -8,7 +8,7 @@ import io.swagger.model.FilterBy; import io.swagger.model.GeoJSONSearchArea; -import io.swagger.model.sort.SortByEntry; +import io.swagger.model.sort.SortByElement; import io.swagger.model.sort.SortOrder; public class SearchQueryBuilder { @@ -22,8 +22,8 @@ public class SearchQueryBuilder { private Class clazz; public SearchQueryBuilder(Class clazz) { - this.selectClause = "SELECT distinct entity FROM " + clazz.getSimpleName() + " entity "; - this.selectOnlyIds = "SELECT distinct entity.id FROM " + clazz.getSimpleName() + " entity "; + this.selectClause = "SELECT entity FROM " + clazz.getSimpleName() + " entity "; + this.selectOnlyIds = "SELECT entity.id FROM " + clazz.getSimpleName() + " entity "; this.whereClause = "WHERE 1=1 "; this.defaultSort = " ORDER BY entity.id ASC "; this.sortClause = ""; @@ -307,6 +307,10 @@ private String entityPrefix(String field) { } } + private String addInfoPrefix(String field) { + return "function('jsonb_extract_path_text', entity.additionalInfo, '" + field + "' ) "; + } + private String paramFilter(String param) { if (param == null) return ""; @@ -335,32 +339,27 @@ public SearchQueryBuilder withSort(String sortByStr, SortOrder sortOrder) { * - An order (DESC, ASC) * - A boolean denoting whether the column to be sorted is data stored in additional info */ - public SearchQueryBuilder sortBy(List sortBy) { + public SearchQueryBuilder sortBy(List sortBy) { if (sortBy == null || sortBy.isEmpty()) { return this; } - for (SortByEntry sort : sortBy) { + for (SortByElement sort : sortBy) { if (sortBy.getFirst().equals(sort)) { this.sortClause += " ORDER BY "; buildSort(sort); + } else { + this.sortClause += ", "; + buildSort(sort); } - - this.sortClause += ", "; - buildSort(sort); } return this; } - private void buildSort(SortByEntry sort) { - if (sort.isAddInfoColumn()) { - // TODO: This assumes the jsonb value of the key is always text. Might need to support numerical sort. - this.sortClause += "additional_info ->>" + sort.getSortedOn() + " " + sort.getSortOrder() + " "; - } else { - this.sortClause += sort.getSortedOn() + " " + sort.getSortOrder() + " "; - } + private void buildSort(SortByElement sort) { + this.sortClause += entityPrefix(sort.getSortedOn()) + " " + sort.getSortOrder() + " "; } /** @@ -373,13 +372,7 @@ private void buildSort(SortByEntry sort) { */ public SearchQueryBuilder filterBy(List filterBy) { for (FilterBy filter : filterBy) { - if (filter.isAddInfoColumn()) { - // TODO: This assumes the jsonb value of the key is always text. Might need to support numerical sort. - this.whereClause += " AND additional_info ->> " + filter.getFilterColumn() + " = " + filter.getValue() + " "; - } - else { - this.whereClause += " AND additional_info ->> " + filter.getFilterColumn() + " " + filter.getValue() + " "; - } + this.whereClause += " AND " + entityPrefix(filter.getFilterColumn()) + " LIKE '%" + filter.getValue() + "%' "; } return this; diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/core/TrialService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/core/TrialService.java index a9ee35cc..6217f755 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/core/TrialService.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/core/TrialService.java @@ -4,7 +4,7 @@ import java.util.stream.Collectors; import io.swagger.model.core.*; -import io.swagger.model.sort.SortByEntry; +import io.swagger.model.sort.SortByElement; import io.swagger.model.sort.SortOrder; import jakarta.validation.Valid; @@ -97,8 +97,8 @@ public List findTrials(@Valid String commonCropName, @Valid String contac if (searchDateRangeEnd != null) request.setSearchDateRangeEnd(searchDateRangeEnd); if (sortBy != null) { - SortByEntry querySortBy = new SortByEntry(sortBy, SortOrder.valueOf(sortOrder), false); - request.setSortByEntry(List.of()); + SortByElement querySortBy = new SortByElement(sortBy, SortOrder.valueOf(sortOrder), false); + request.setSortByEntry(List.of(querySortBy)); } request.addExternalReferenceItem(externalReferenceId, externalReferenceID, externalReferenceSource); return findTrials(request, metadata); From 2d5128502765c616c7328dcf7331bc859a2dbac9 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Thu, 21 May 2026 19:22:15 -0400 Subject: [PATCH 04/26] Preliminary pass at allowed fields mapping --- .../java/io/swagger/model/SearchRequest.java | 62 +++++++++++++++++-- .../model/core/TrialSearchRequest.java | 20 +++++- 2 files changed, 75 insertions(+), 7 deletions(-) diff --git a/src/main/java/io/swagger/model/SearchRequest.java b/src/main/java/io/swagger/model/SearchRequest.java index 9d9695c8..cb29ee2c 100644 --- a/src/main/java/io/swagger/model/SearchRequest.java +++ b/src/main/java/io/swagger/model/SearchRequest.java @@ -3,9 +3,12 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.model.sort.SortByElement; +import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; +import org.springframework.http.HttpStatus; import java.util.ArrayList; import java.util.List; +import java.util.Map; public abstract class SearchRequest { @JsonIgnore @@ -27,11 +30,19 @@ public abstract class SearchRequest { protected List externalReferenceSources = null; @JsonProperty("filterBy") - protected FilterBy filterBy = null; + protected List filterBy = null; @JsonProperty("sortBy") protected List sortBy = null; + @JsonIgnore + protected Map sortFilterEntityColumnNamesByRequestName = null; + + @JsonIgnore + public List getExternalReferenceIds() { + return externalReferenceIds; + } + final public SearchRequest page(Integer page) { this.page = page; return this; @@ -127,11 +138,30 @@ public void addExternalReferenceItem(String externalReferenceId, String external } - public FilterBy getFilterBy() { + public List getFilterBy() { return filterBy; } - public void setFilterBy(FilterBy filterBy) { + public void setFilterBy(List filterBy) throws BrAPIServerException { + + if (filterBy == null || filterBy.isEmpty()) { + return; + } + + Map allowedSortFilterNames = getSortFilterEntityColumnNamesByRequestName(); + + for (FilterBy filterByItem : filterBy) { + String filterColumnEntityName = getSortFilterEntityColumnNamesByRequestName().get(filterByItem.getFilterColumn()); + + if (filterColumnEntityName == null) { + throw new BrAPIServerException(HttpStatus.BAD_REQUEST, + String.format("Supplied filterColumn [%s] not available in allowed names [%s]", filterByItem.getFilterColumn(), allowedSortFilterNames.keySet()) + ); + } else { + // Remap suppliedFilterColumn to actual entity name supplied by mapper + filterByItem.setFilterColumn(filterColumnEntityName); + } + } this.filterBy = filterBy; } @@ -139,7 +169,31 @@ public List getSortByEntry() { return sortBy; } - public void setSortByEntry(List sortBy) { + public void setSortByEntry(List sortBy) throws BrAPIServerException { + + if (sortBy == null || sortBy.isEmpty()) { + return; + } + + Map allowedSortFilterNames = getSortFilterEntityColumnNamesByRequestName(); + + for (SortByElement sortByItem : sortBy) { + String filterColumnEntityName = getSortFilterEntityColumnNamesByRequestName().get(sortByItem.getSortedOn()); + + if (filterColumnEntityName == null) { + throw new BrAPIServerException(HttpStatus.BAD_REQUEST, + String.format("Supplied sortColumn [%s] not available in allowed names [%s]", sortByItem.getSortedOn(), allowedSortFilterNames.keySet()) + ); + } else { + // Remap suppliedFilterColumn to actual entity name supplied by mapper + sortByItem.setSortedOn(filterColumnEntityName); + } + } + this.sortBy = sortBy; } + + public Map getSortFilterEntityColumnNamesByRequestName() { + throw new UnsupportedOperationException(String.format("Sort/Filtering not implemented for %s", this.getClass().getSimpleName())); + } } diff --git a/src/main/java/io/swagger/model/core/TrialSearchRequest.java b/src/main/java/io/swagger/model/core/TrialSearchRequest.java index 5a4b741f..324774fa 100644 --- a/src/main/java/io/swagger/model/core/TrialSearchRequest.java +++ b/src/main/java/io/swagger/model/core/TrialSearchRequest.java @@ -1,14 +1,23 @@ package io.swagger.model.core; -import java.util.Objects; +import java.util.*; + import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.model.SearchRequest; -import java.util.ArrayList; -import java.util.List; import java.time.LocalDate; public class TrialSearchRequest extends SearchRequest { + + // Key - allowed sort or field filter name for this entity + // Value = entity field name that represents the submitted field. Used later on in query building. + private static final Map ALLOWED_SORT_AND_FILTER_FIELDS = + Map.of( + "trialName", "trialName", + "createdDate", "createdDate", + "createdBy", "createdBy" + ); + @JsonProperty("commonCropNames") private List commonCropNames = null; @@ -424,4 +433,9 @@ public Integer getTotalParameterCount() { count += this.trialPUIs.size(); return count; } + + @Override + public Map getSortFilterEntityColumnNamesByRequestName() { + return ALLOWED_SORT_AND_FILTER_FIELDS; + } } From f9440f06ea342f59e25b9fdb8ff8846eb8acb434 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Fri, 22 May 2026 15:42:16 -0400 Subject: [PATCH 05/26] Add parametrization logic for filter values, infrastructure for 400 errors on serialization --- src/main/java/io/swagger/model/FilterBy.java | 11 +++--- .../java/io/swagger/model/SearchRequest.java | 32 ++++++++++++---- .../BrapiExceptionHandler.java | 37 +++++++++++++++++-- .../model/entity/core/TrialEntity.java | 4 +- .../service/SearchQueryBuilder.java | 28 +++++++++++--- .../service/core/TrialService.java | 6 ++- 6 files changed, 92 insertions(+), 26 deletions(-) diff --git a/src/main/java/io/swagger/model/FilterBy.java b/src/main/java/io/swagger/model/FilterBy.java index 001ea05b..acbc03e7 100644 --- a/src/main/java/io/swagger/model/FilterBy.java +++ b/src/main/java/io/swagger/model/FilterBy.java @@ -1,15 +1,16 @@ package io.swagger.model; public class FilterBy { - private String filterColumn; + + private String filterOn; private String value; - public String getFilterColumn() { - return filterColumn; + public String getFilterOn() { + return filterOn; } - public void setFilterColumn(String filterColumn) { - this.filterColumn = filterColumn; + public void setFilterOn(String filterOn) { + this.filterOn = filterOn; } public String getValue() { diff --git a/src/main/java/io/swagger/model/SearchRequest.java b/src/main/java/io/swagger/model/SearchRequest.java index cb29ee2c..cefd14bd 100644 --- a/src/main/java/io/swagger/model/SearchRequest.java +++ b/src/main/java/io/swagger/model/SearchRequest.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; import io.swagger.model.sort.SortByElement; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; import org.springframework.http.HttpStatus; @@ -142,6 +143,7 @@ public List getFilterBy() { return filterBy; } + @JsonSetter("filterBy") public void setFilterBy(List filterBy) throws BrAPIServerException { if (filterBy == null || filterBy.isEmpty()) { @@ -151,25 +153,35 @@ public void setFilterBy(List filterBy) throws BrAPIServerException { Map allowedSortFilterNames = getSortFilterEntityColumnNamesByRequestName(); for (FilterBy filterByItem : filterBy) { - String filterColumnEntityName = getSortFilterEntityColumnNamesByRequestName().get(filterByItem.getFilterColumn()); + + if (filterByItem.getFilterOn() == null || filterByItem.getFilterOn().isEmpty()) { + throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "filterOn attribute not provided in element of filterBy list."); + } + + if (filterByItem.getValue() == null || filterByItem.getValue().isEmpty()) { + throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "value attribute not provided in element of filterBy list."); + } + + String filterColumnEntityName = getSortFilterEntityColumnNamesByRequestName().get(filterByItem.getFilterOn()); if (filterColumnEntityName == null) { throw new BrAPIServerException(HttpStatus.BAD_REQUEST, - String.format("Supplied filterColumn [%s] not available in allowed names [%s]", filterByItem.getFilterColumn(), allowedSortFilterNames.keySet()) + String.format("Supplied filterColumn [%s] not available in allowed names [%s]", filterByItem.getFilterOn(), allowedSortFilterNames.keySet()) ); } else { // Remap suppliedFilterColumn to actual entity name supplied by mapper - filterByItem.setFilterColumn(filterColumnEntityName); + filterByItem.setFilterOn(filterColumnEntityName); } } this.filterBy = filterBy; } - public List getSortByEntry() { + public List getSortByElements() { return sortBy; } - public void setSortByEntry(List sortBy) throws BrAPIServerException { + @JsonSetter("sortBy") + public void setSortByElements(List sortBy) throws BrAPIServerException { if (sortBy == null || sortBy.isEmpty()) { return; @@ -178,15 +190,19 @@ public void setSortByEntry(List sortBy) throws BrAPIServerExcepti Map allowedSortFilterNames = getSortFilterEntityColumnNamesByRequestName(); for (SortByElement sortByItem : sortBy) { - String filterColumnEntityName = getSortFilterEntityColumnNamesByRequestName().get(sortByItem.getSortedOn()); + if (sortByItem.getSortedOn() == null || sortByItem.getSortedOn().isEmpty()) { + throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "sortedOn attribute not provided in element of sortBy list"); + } - if (filterColumnEntityName == null) { + String sortColumnEntityName = getSortFilterEntityColumnNamesByRequestName().get(sortByItem.getSortedOn()); + + if (sortColumnEntityName == null) { throw new BrAPIServerException(HttpStatus.BAD_REQUEST, String.format("Supplied sortColumn [%s] not available in allowed names [%s]", sortByItem.getSortedOn(), allowedSortFilterNames.keySet()) ); } else { // Remap suppliedFilterColumn to actual entity name supplied by mapper - sortByItem.setSortedOn(filterColumnEntityName); + sortByItem.setSortedOn(sortColumnEntityName); } } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/BrapiExceptionHandler.java b/src/main/java/org/brapi/test/BrAPITestServer/BrapiExceptionHandler.java index 7ee15a60..fa6508f0 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/BrapiExceptionHandler.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/BrapiExceptionHandler.java @@ -1,13 +1,12 @@ package org.brapi.test.BrAPITestServer; import java.util.ArrayList; +import java.util.Map; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.http.*; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.lang.Nullable; import org.springframework.web.bind.MissingServletRequestParameterException; @@ -70,4 +69,36 @@ private ResponseEntity buildErrorResponse(HttpStatus code, String messag return new ResponseEntity(apiError, code); } + + // This override handles JSON parsing failures detected by Jackson. + // It allows for capture of BrAPI generated BAD Request exceptions that are occur during serialization, + // like those involved in filtering and sorting in search requests. + @Override + protected ResponseEntity handleHttpMessageNotReadable( + HttpMessageNotReadableException ex, + HttpHeaders headers, + HttpStatusCode status, + WebRequest request + ) { + Throwable root = ex.getMostSpecificCause(); + + if (root instanceof BrAPIServerException brapiServerException) { + + ProblemDetail detail = + ProblemDetail.forStatus(HttpStatus.BAD_REQUEST); + + detail.setTitle("Bad Request"); + detail.setDetail(brapiServerException.getResponseMessage()); + + return ResponseEntity.badRequest().body(detail); + } + + // Delegate back to Spring default behavior + return super.handleHttpMessageNotReadable( + ex, + headers, + status, + request + ); + } } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/model/entity/core/TrialEntity.java b/src/main/java/org/brapi/test/BrAPITestServer/model/entity/core/TrialEntity.java index f18543fc..5f9bb989 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/model/entity/core/TrialEntity.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/model/entity/core/TrialEntity.java @@ -40,8 +40,8 @@ public class TrialEntity extends BrAPIPrimaryEntity { @Column(name = "soft_deleted") private boolean softDeleted; - @Formula("(to_timestamp(additional_info #>> '{createdDate}', 'YYYY-MM-DD'))") - private OffsetDateTime createdDate; + @Formula("(additional_info #>> '{createdDate}')") + private String createdDate; @Formula("(additional_info #>> '{createdBy,userName}')") private String createdBy; diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java index 41a423ec..86f4f37e 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -10,6 +10,8 @@ import io.swagger.model.GeoJSONSearchArea; import io.swagger.model.sort.SortByElement; import io.swagger.model.sort.SortOrder; +import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; +import org.springframework.http.HttpStatus; public class SearchQueryBuilder { @@ -135,6 +137,16 @@ public SearchQueryBuilder appendSingle(UUID single, String columnName) { return this; } + public SearchQueryBuilder appendLike(String like, String columnName) { + String paramName = paramFilter(columnName); + + if (like != null) { + this.whereClause += "AND " + entityPrefix(columnName) + " LIKE :" + paramName + " "; + this.params.put(paramName, "%" + like + "%"); + } + return this; + } + public > SearchQueryBuilder appendEnum(E enumVal, String columnName) { String paramName = paramFilter(columnName); if (enumVal != null) { @@ -337,9 +349,8 @@ public SearchQueryBuilder withSort(String sortByStr, SortOrder sortOrder) { * A SortBy has * - A column name * - An order (DESC, ASC) - * - A boolean denoting whether the column to be sorted is data stored in additional info */ - public SearchQueryBuilder sortBy(List sortBy) { + public SearchQueryBuilder sortBy(List sortBy) throws BrAPIServerException { if (sortBy == null || sortBy.isEmpty()) { return this; @@ -368,12 +379,17 @@ private void buildSort(SortByElement sort) { * * A FilterBy has * - A column name - * - A boolean denoting whether the column to be sorted is data stored in additional info */ - public SearchQueryBuilder filterBy(List filterBy) { + public SearchQueryBuilder filterBy(List filterBy) throws BrAPIServerException { + SearchQueryBuilder searchQuery = this; + + if (filterBy == null || filterBy.isEmpty()) { + return searchQuery; + } + for (FilterBy filter : filterBy) { - this.whereClause += " AND " + entityPrefix(filter.getFilterColumn()) + " LIKE '%" + filter.getValue() + "%' "; + searchQuery = appendLike(filter.getValue(), filter.getFilterOn()); } - return this; + return searchQuery; }} diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/core/TrialService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/core/TrialService.java index 6217f755..c01622d4 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/core/TrialService.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/core/TrialService.java @@ -97,8 +97,9 @@ public List findTrials(@Valid String commonCropName, @Valid String contac if (searchDateRangeEnd != null) request.setSearchDateRangeEnd(searchDateRangeEnd); if (sortBy != null) { + // TODO: Fix this use case by adding allowable sort fields for trials from old enum in here SortByElement querySortBy = new SortByElement(sortBy, SortOrder.valueOf(sortOrder), false); - request.setSortByEntry(List.of(querySortBy)); + request.setSortByElements(List.of(querySortBy)); } request.addExternalReferenceItem(externalReferenceId, externalReferenceID, externalReferenceSource); return findTrials(request, metadata); @@ -127,7 +128,8 @@ public List findTrials(@Valid TrialSearchRequest request, Metadata metada .appendList(request.getStudyNames(), "*study.studyName").appendList(request.getTrialDbIds(), "id") .appendList(request.getTrialNames(), "trialName") .appendDateRange(request.getSearchDateRangeStart(), request.getSearchDateRangeEnd(), "startDate") - .sortBy(request.getSortByEntry()); + .sortBy(request.getSortByElements()) + .filterBy(request.getFilterBy()); Page trialsPage = trialRepository.findAllBySearchAndPaginate(searchQuery, pageReq); PagingUtility.calculateMetaData(metadata, trialsPage); From 90214e06326e2f204b645df7bf45f861e5e92071 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Fri, 22 May 2026 17:06:44 -0400 Subject: [PATCH 06/26] Use new SortBy class in all use cases. --- .../java/io/swagger/model/SearchRequest.java | 12 ++-- .../java/io/swagger/model/core/SortBy.java | 58 ----------------- .../model/core/StudySearchRequest.java | 63 ++++++++----------- .../model/core/TrialSearchRequest.java | 6 +- .../sort/{SortByElement.java => SortBy.java} | 10 ++- .../service/SearchQueryBuilder.java | 9 ++- .../service/core/StudyService.java | 56 +++-------------- .../service/core/TrialService.java | 7 +-- 8 files changed, 53 insertions(+), 168 deletions(-) delete mode 100644 src/main/java/io/swagger/model/core/SortBy.java rename src/main/java/io/swagger/model/sort/{SortByElement.java => SortBy.java} (63%) diff --git a/src/main/java/io/swagger/model/SearchRequest.java b/src/main/java/io/swagger/model/SearchRequest.java index cefd14bd..828cd69c 100644 --- a/src/main/java/io/swagger/model/SearchRequest.java +++ b/src/main/java/io/swagger/model/SearchRequest.java @@ -3,7 +3,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSetter; -import io.swagger.model.sort.SortByElement; +import io.swagger.model.sort.SortBy; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; import org.springframework.http.HttpStatus; @@ -34,7 +34,7 @@ public abstract class SearchRequest { protected List filterBy = null; @JsonProperty("sortBy") - protected List sortBy = null; + protected List sortBy = null; @JsonIgnore protected Map sortFilterEntityColumnNamesByRequestName = null; @@ -143,7 +143,6 @@ public List getFilterBy() { return filterBy; } - @JsonSetter("filterBy") public void setFilterBy(List filterBy) throws BrAPIServerException { if (filterBy == null || filterBy.isEmpty()) { @@ -176,12 +175,11 @@ public void setFilterBy(List filterBy) throws BrAPIServerException { this.filterBy = filterBy; } - public List getSortByElements() { + public List getSortByElements() { return sortBy; } - @JsonSetter("sortBy") - public void setSortByElements(List sortBy) throws BrAPIServerException { + public void setSortBy(List sortBy) throws BrAPIServerException { if (sortBy == null || sortBy.isEmpty()) { return; @@ -189,7 +187,7 @@ public void setSortByElements(List sortBy) throws BrAPIServerExce Map allowedSortFilterNames = getSortFilterEntityColumnNamesByRequestName(); - for (SortByElement sortByItem : sortBy) { + for (SortBy sortByItem : sortBy) { if (sortByItem.getSortedOn() == null || sortByItem.getSortedOn().isEmpty()) { throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "sortedOn attribute not provided in element of sortBy list"); } diff --git a/src/main/java/io/swagger/model/core/SortBy.java b/src/main/java/io/swagger/model/core/SortBy.java deleted file mode 100644 index bf215cc5..00000000 --- a/src/main/java/io/swagger/model/core/SortBy.java +++ /dev/null @@ -1,58 +0,0 @@ -package io.swagger.model.core; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonValue; - -@Deprecated -// TODO: Phase out in favor of io.swagger.model.sort objects -public enum SortBy { - - STUDYDBID("studyDbId"), - - STARTDATE("startDate"), - - ENDDATE("endDate"), - - TRIALDBID("trialDbId"), - - TRIALNAME("trialName"), - - PROGRAMDBID("programDbId"), - - LOCATIONDBID("locationDbId"), - - SEASONDBID("seasonDbId"), - - STUDYTYPE("studyType"), - - STUDYNAME("studyName"), - - STUDYLOCATION("studyLocation"), - - PROGRAMNAME("programName"), - - GERMPLASMDBID("germplasmDbId"), - - OBSERVATIONVARIABLEDBID("observationVariableDbId"); -private String value; - - SortBy(String value) { - this.value = value; - } - - @Override - @JsonValue - public String toString() { - return String.valueOf(value); - } - - @JsonCreator - public static SortBy fromValue(String text) { - for (SortBy b : SortBy.values()) { - if (String.valueOf(b.value).equals(text)) { - return b; - } - } - return null; - } -} diff --git a/src/main/java/io/swagger/model/core/StudySearchRequest.java b/src/main/java/io/swagger/model/core/StudySearchRequest.java index 26849475..78ed90d0 100644 --- a/src/main/java/io/swagger/model/core/StudySearchRequest.java +++ b/src/main/java/io/swagger/model/core/StudySearchRequest.java @@ -1,16 +1,34 @@ package io.swagger.model.core; +import java.util.Map; import java.util.Objects; import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.model.SearchRequest; -import io.swagger.model.sort.SortOrder; import java.util.ArrayList; import java.util.List; public class StudySearchRequest extends SearchRequest { + + // Key - allowed sort or field filter name for this entity + // Value = entity field name that represents the submitted field. Used later on in query building. + private static final Map ALLOWED_SORT_AND_FILTER_FIELDS = + Map.ofEntries( + Map.entry("germplasmDbId", "*obsunit.germplasm.id"), + Map.entry("locationDbId", "location.id"), + Map.entry("observationVariableDbId", "*observation.observationVariable.id"), + Map.entry("programDbId", "trial.program.id"), + Map.entry("programName", "trial.program.name"), + Map.entry("seasonDbId", "*season.id"), + Map.entry("studyDbId", "id"), + Map.entry("studyLocation", "location.id"), + Map.entry("trialDbId", "trial.id"), + Map.entry("studyType", "studyName"), + Map.entry("studyName", "studyName") + ); + @JsonProperty("commonCropNames") private List commonCropNames = null; @@ -56,12 +74,6 @@ public class StudySearchRequest extends SearchRequest { @JsonProperty("seasonDbIds") private List seasonDbIds = null; - @JsonProperty("sortBy") - private SortBy sortBy = null; - - @JsonProperty("sortOrder") - private SortOrder sortOrder = null; - @JsonProperty("studyCodes") private List studyCodes = null; @@ -378,32 +390,6 @@ public void setSeasonDbIds(List seasonDbIds) { this.seasonDbIds = seasonDbIds; } - public StudySearchRequest sortBy(SortBy sortBy) { - this.sortBy = sortBy; - return this; - } - - public SortBy getSortBy() { - return sortBy; - } - - public void setSortBy(SortBy sortBy) { - this.sortBy = sortBy; - } - - public StudySearchRequest sortOrder(SortOrder sortOrder) { - this.sortOrder = sortOrder; - return this; - } - - public SortOrder getSortOrder() { - return sortOrder; - } - - public void setSortOrder(SortOrder sortOrder) { - this.sortOrder = sortOrder; - } - public StudySearchRequest studyCodes(List studyCodes) { this.studyCodes = studyCodes; return this; @@ -494,7 +480,6 @@ public boolean equals(java.lang.Object o) { && Objects.equals(this.active, studySearchRequest.active) && Objects.equals(this.seasonDbIds, studySearchRequest.seasonDbIds) && Objects.equals(this.sortBy, studySearchRequest.sortBy) - && Objects.equals(this.sortOrder, studySearchRequest.sortOrder) && Objects.equals(this.studyCodes, studySearchRequest.studyCodes) && Objects.equals(this.studyPUIs, studySearchRequest.studyPUIs) && Objects.equals(this.studyTypes, studySearchRequest.studyTypes) && super.equals(o); @@ -505,7 +490,7 @@ public int hashCode() { return Objects.hash(commonCropNames, programDbIds, programNames, trialDbIds, trialNames, studyDbIds, studyNames, locationDbIds, locationNames, germplasmDbIds, germplasmNames, observationVariableDbIds, observationVariableNames, externalReferenceIds, externalReferenceSources, active, seasonDbIds, sortBy, - sortOrder, studyCodes, studyPUIs, studyTypes, super.hashCode()); + studyCodes, studyPUIs, studyTypes, super.hashCode()); } @Override @@ -531,7 +516,6 @@ public String toString() { sb.append(" active: ").append(toIndentedString(active)).append("\n"); sb.append(" seasonDbIds: ").append(toIndentedString(seasonDbIds)).append("\n"); sb.append(" sortBy: ").append(toIndentedString(sortBy)).append("\n"); - sb.append(" sortOrder: ").append(toIndentedString(sortOrder)).append("\n"); sb.append(" studyCodes: ").append(toIndentedString(studyCodes)).append("\n"); sb.append(" studyPUIs: ").append(toIndentedString(studyPUIs)).append("\n"); sb.append(" studyTypes: ").append(toIndentedString(studyTypes)).append("\n"); @@ -585,8 +569,6 @@ public Integer getTotalParameterCount() { count += this.seasonDbIds.size(); if (this.sortBy != null) count += 1; - if (this.sortOrder != null) - count += 1; if (this.studyCodes != null) count += this.studyCodes.size(); if (this.studyPUIs != null) @@ -595,4 +577,9 @@ public Integer getTotalParameterCount() { count += this.studyTypes.size(); return count; } + + @Override + public Map getSortFilterEntityColumnNamesByRequestName() { + return ALLOWED_SORT_AND_FILTER_FIELDS; + } } diff --git a/src/main/java/io/swagger/model/core/TrialSearchRequest.java b/src/main/java/io/swagger/model/core/TrialSearchRequest.java index 324774fa..aaed24eb 100644 --- a/src/main/java/io/swagger/model/core/TrialSearchRequest.java +++ b/src/main/java/io/swagger/model/core/TrialSearchRequest.java @@ -15,7 +15,11 @@ public class TrialSearchRequest extends SearchRequest { Map.of( "trialName", "trialName", "createdDate", "createdDate", - "createdBy", "createdBy" + "createdBy", "createdBy", + "trialDbId", "id", + "programDbId","program.id", + "startDate", "startDate", + "endDate", "endDate" ); @JsonProperty("commonCropNames") diff --git a/src/main/java/io/swagger/model/sort/SortByElement.java b/src/main/java/io/swagger/model/sort/SortBy.java similarity index 63% rename from src/main/java/io/swagger/model/sort/SortByElement.java rename to src/main/java/io/swagger/model/sort/SortBy.java index cbc79600..bb286fa6 100644 --- a/src/main/java/io/swagger/model/sort/SortByElement.java +++ b/src/main/java/io/swagger/model/sort/SortBy.java @@ -1,18 +1,16 @@ package io.swagger.model.sort; -// TODO: Replace io.swagger.model.core.SortBy with this class and rename this class to SortBy -public class SortByElement { +public class SortBy { private String sortedOn; private SortOrder sortOrder = SortOrder.ASC; - public SortByElement(String sortedOn, - SortOrder sortOrder, - boolean addInfoColumn) { + public SortBy(String sortedOn, + SortOrder sortOrder) { this.sortedOn = sortedOn; this.sortOrder = sortOrder; } - public SortByElement() {} + public SortBy() {} public String getSortedOn() { return sortedOn; diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java index 86f4f37e..4961ed14 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -8,10 +8,9 @@ import io.swagger.model.FilterBy; import io.swagger.model.GeoJSONSearchArea; -import io.swagger.model.sort.SortByElement; +import io.swagger.model.sort.SortBy; import io.swagger.model.sort.SortOrder; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; -import org.springframework.http.HttpStatus; public class SearchQueryBuilder { @@ -350,13 +349,13 @@ public SearchQueryBuilder withSort(String sortByStr, SortOrder sortOrder) { * - A column name * - An order (DESC, ASC) */ - public SearchQueryBuilder sortBy(List sortBy) throws BrAPIServerException { + public SearchQueryBuilder sortBy(List sortBy) throws BrAPIServerException { if (sortBy == null || sortBy.isEmpty()) { return this; } - for (SortByElement sort : sortBy) { + for (SortBy sort : sortBy) { if (sortBy.getFirst().equals(sort)) { this.sortClause += " ORDER BY "; buildSort(sort); @@ -369,7 +368,7 @@ public SearchQueryBuilder sortBy(List sortBy) throws BrAPIServ return this; } - private void buildSort(SortByElement sort) { + private void buildSort(SortBy sort) { this.sortClause += entityPrefix(sort.getSortedOn()) + " " + sort.getSortOrder() + " "; } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/core/StudyService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/core/StudyService.java index 4c0cb198..e5611ab7 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/core/StudyService.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/core/StudyService.java @@ -3,6 +3,7 @@ import java.util.*; import java.util.stream.Collectors; +import io.swagger.model.sort.SortBy; import org.apache.commons.lang3.StringUtils; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerDbIdNotFoundException; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; @@ -37,7 +38,6 @@ import io.swagger.model.Metadata; import io.swagger.model.core.Contact; import io.swagger.model.core.EnvironmentParameter; -import io.swagger.model.core.SortBy; import io.swagger.model.sort.SortOrder; import io.swagger.model.core.Study; import io.swagger.model.core.StudyExperimentalDesign; @@ -107,10 +107,11 @@ public List findStudies(String commonCropName, String studyType, String p request.addObservationVariableDbIdsItem(observationVariableDbId); if (active != null) request.setActive(active); - if (sortBy != null && SortBy.fromValue(sortBy) != null) - request.setSortBy(SortBy.fromValue(sortBy)); - if (sortOrder != null && SortOrder.fromValue(sortOrder) != null) - request.setSortOrder(SortOrder.fromValue(sortOrder)); + if (sortBy != null) { + SortBy sortByElement = new SortBy(sortBy, SortOrder.valueOf(sortOrder)); + + request.setSortBy(List.of(sortByElement)); + } request.addExternalReferenceItem(externalReferenceId, externalReferenceID, externalReferenceSource); @@ -158,7 +159,7 @@ public List findStudies(StudySearchRequest request, Metadata metaData) .appendList(request.getStudyDbIds(), "id").appendList(request.getStudyNames(), "studyName") .appendList(request.getStudyPUIs(), "studyPUI").appendList(request.getStudyTypes(), "studyType") .appendList(request.getTrialDbIds(), "trial.id").appendList(request.getTrialNames(), "trial.trialName") - .withSort(getSortByField(request.getSortBy()), request.getSortOrder()); + .sortBy(request.getSortByElements()); Page studiesPage = studyRepository.findAllBySearchAndPaginate(searchQuery, pageReq); PagingUtility.calculateMetaData(metaData, studiesPage); @@ -565,47 +566,4 @@ private EnvironmentParametersEntity convertToEntity(EnvironmentParameter param) return entity; } - private String getSortByField(SortBy sortBy) { - String sortByStr = "id"; - if (sortBy != null) { - switch (sortBy) { - case GERMPLASMDBID: - sortByStr = "*obsunit.germplasm.id"; - break; - case LOCATIONDBID: - sortByStr = "location.id"; - break; - case OBSERVATIONVARIABLEDBID: - sortByStr = "*observation.observationVariable.id"; - break; - case PROGRAMDBID: - sortByStr = "trial.program.id"; - break; - case PROGRAMNAME: - sortByStr = "trial.program.name"; - break; - case SEASONDBID: - sortByStr = "*season.id"; - break; - case STUDYDBID: - sortByStr = "id"; - break; - case STUDYLOCATION: - sortByStr = "location.id"; - break; - case TRIALDBID: - sortByStr = "trial.id "; - break; - case STUDYTYPE: - sortByStr = "studyName"; - break; - case STUDYNAME: - default: - sortByStr = "studyName"; - break; - } - } - return sortByStr; - } - } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/core/TrialService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/core/TrialService.java index c01622d4..21146a30 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/core/TrialService.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/core/TrialService.java @@ -4,7 +4,7 @@ import java.util.stream.Collectors; import io.swagger.model.core.*; -import io.swagger.model.sort.SortByElement; +import io.swagger.model.sort.SortBy; import io.swagger.model.sort.SortOrder; import jakarta.validation.Valid; @@ -97,9 +97,8 @@ public List findTrials(@Valid String commonCropName, @Valid String contac if (searchDateRangeEnd != null) request.setSearchDateRangeEnd(searchDateRangeEnd); if (sortBy != null) { - // TODO: Fix this use case by adding allowable sort fields for trials from old enum in here - SortByElement querySortBy = new SortByElement(sortBy, SortOrder.valueOf(sortOrder), false); - request.setSortByElements(List.of(querySortBy)); + SortBy querySortBy = new SortBy(sortBy, SortOrder.valueOf(sortOrder)); + request.setSortBy(List.of(querySortBy)); } request.addExternalReferenceItem(externalReferenceId, externalReferenceID, externalReferenceSource); return findTrials(request, metadata); From 90e77dd854c47f07cfe002811b23e1ab8645e13c Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Fri, 29 May 2026 15:50:12 -0400 Subject: [PATCH 07/26] Make filter case insensitive Additionally add active as a filter column --- .../swagger/model/core/TrialSearchRequest.java | 3 ++- .../service/SearchQueryBuilder.java | 17 ++--------------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/src/main/java/io/swagger/model/core/TrialSearchRequest.java b/src/main/java/io/swagger/model/core/TrialSearchRequest.java index aaed24eb..ce24b1f7 100644 --- a/src/main/java/io/swagger/model/core/TrialSearchRequest.java +++ b/src/main/java/io/swagger/model/core/TrialSearchRequest.java @@ -19,7 +19,8 @@ public class TrialSearchRequest extends SearchRequest { "trialDbId", "id", "programDbId","program.id", "startDate", "startDate", - "endDate", "endDate" + "endDate", "endDate", + "active", "active" ); @JsonProperty("commonCropNames") diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java index 4961ed14..61ca21f8 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -140,7 +140,7 @@ public SearchQueryBuilder appendLike(String like, String columnName) { String paramName = paramFilter(columnName); if (like != null) { - this.whereClause += "AND " + entityPrefix(columnName) + " LIKE :" + paramName + " "; + this.whereClause += "AND lower(" + entityPrefix(columnName) + ") LIKE :" + paramName + " "; this.params.put(paramName, "%" + like + "%"); } return this; @@ -328,19 +328,6 @@ private String paramFilter(String param) { return param.replace('.', '_').replace('*', '_'); } - @Deprecated - // Use withSortBy instead - public SearchQueryBuilder withSort(String sortByStr, SortOrder sortOrder) { - String sortOrderStr = "ASC"; - if (sortOrder != null) { - sortOrderStr = sortOrder.toString(); - } - - this.sortClause += " ORDER BY " + entityPrefix(sortByStr) + " " + sortOrderStr; - - return this; - } - /** * Takes a list of SortBy options that should typically come in a searchRequest. * Applies the entries in the list to sort the SearchQuery. @@ -387,7 +374,7 @@ public SearchQueryBuilder filterBy(List filterBy) throws BrAPIServe } for (FilterBy filter : filterBy) { - searchQuery = appendLike(filter.getValue(), filter.getFilterOn()); + searchQuery = appendLike(filter.getValue().toLowerCase(), filter.getFilterOn()); } return searchQuery; From 3bf581ada9b97adcff06bd7346a40391f5ce01a4 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Mon, 1 Jun 2026 15:48:51 -0400 Subject: [PATCH 08/26] Fix pom jackson depedency issues --- pom.xml | 54 ++++++++++++++---------------------------------------- 1 file changed, 14 insertions(+), 40 deletions(-) diff --git a/pom.xml b/pom.xml index 962d101a..a1e201c5 100644 --- a/pom.xml +++ b/pom.xml @@ -20,6 +20,7 @@ 21 2.10.0 2.0.16 + 2.18.1 @@ -73,28 +74,11 @@ io.jsonwebtoken jjwt 0.9.1 - - - com.fasterxml.jackson.core - jackson-databind - - com.google.api-client google-api-client 1.27.0 - - - com.fasterxml.jackson.core - jackson-core - - - - - com.fasterxml.jackson.core - jackson-databind - [2.9.9.1,) io.springfox @@ -106,11 +90,6 @@ springfox-swagger-ui ${springfox-version} - - com.github.joschi.jackson - jackson-datatype-threetenbp - 2.8.4 - jakarta.annotation jakarta.annotation-api @@ -143,6 +122,18 @@ + + + + com.fasterxml.jackson + jackson-bom + ${jackson.version} + pom + import + + + + @@ -161,7 +152,7 @@ org.springframework.boot spring-boot-maven-plugin - 2.7.5 + ${spring.version} @@ -173,21 +164,4 @@ - - - spring-releases - https://repo.spring.io/libs-release - - - spring-milestone - https://repo.spring.io/libs-milestone - - - - - spring-releases - https://repo.spring.io/libs-release - - - From da60a1adb17397979b52db4624c5561850aa03ee Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Tue, 21 Jul 2026 17:46:34 -0400 Subject: [PATCH 09/26] Migrate parent observation ids on ou_level to brapi dbIds --- .../V006_001__migrate_parent_ou_ids.sql | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/main/resources/db/migration/V006_001__migrate_parent_ou_ids.sql diff --git a/src/main/resources/db/migration/V006_001__migrate_parent_ou_ids.sql b/src/main/resources/db/migration/V006_001__migrate_parent_ou_ids.sql new file mode 100644 index 00000000..09533d4f --- /dev/null +++ b/src/main/resources/db/migration/V006_001__migrate_parent_ou_ids.sql @@ -0,0 +1,27 @@ +WITH + levels_and_parent_ou_bi_id AS ( +-- Find observation_unit_level codes that contain a key by checking for a space in the level code. +-- Only other level codes that can exist are block/rep + SELECT + id AS level_id, + substring(level_code FROM '^([^ ]+)') AS parent_ou_id + FROM observation_unit_level + WHERE level_code LIKE '% %' + ), + ou_ids_matched_on_levels AS ( + SELECT + ou.id AS ou_id, + levels_and_parent_ou_bi_id.level_id + FROM observation_unit ou + JOIN observation_unit_external_references ouex ON ou.id = ouex.observation_unit_entity_id + JOIN external_reference ex ON ouex.external_references_id = ex.id + JOIN levels_and_parent_ou_bi_id ON ex.external_reference_id = levels_and_parent_ou_bi_id.parent_ou_id + ) +UPDATE observation_unit_level +SET level_code = regexp_replace( + observation_unit_level.level_code, + '^[^ ]+', + mol.ou_id::text +) +FROM ou_ids_matched_on_levels mol +WHERE observation_unit_level.id = mol.level_id \ No newline at end of file From c2afc76cafd3acf1f3cc20514e2883e13c7966d1 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Tue, 21 Jul 2026 17:59:40 -0400 Subject: [PATCH 10/26] Add some clarifying comments --- .../db/migration/V006_001__migrate_parent_ou_ids.sql | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/resources/db/migration/V006_001__migrate_parent_ou_ids.sql b/src/main/resources/db/migration/V006_001__migrate_parent_ou_ids.sql index 09533d4f..3dd227b5 100644 --- a/src/main/resources/db/migration/V006_001__migrate_parent_ou_ids.sql +++ b/src/main/resources/db/migration/V006_001__migrate_parent_ou_ids.sql @@ -1,21 +1,23 @@ WITH levels_and_parent_ou_bi_id AS ( -- Find observation_unit_level codes that contain a key by checking for a space in the level code. --- Only other level codes that can exist are block/rep +-- Only other level codes that can exist are block/rep. +-- Then extract the uuid from the level code, since program key is also contained there. SELECT id AS level_id, - substring(level_code FROM '^([^ ]+)') AS parent_ou_id + substring(level_code FROM '^([^ ]+)') AS parent_bi_ou_id FROM observation_unit_level WHERE level_code LIKE '% %' ), ou_ids_matched_on_levels AS ( +-- Now match the bi-generated exref ou ids to ex refs ids, and keep observation_unit_level ids for matching in next part SELECT ou.id AS ou_id, levels_and_parent_ou_bi_id.level_id FROM observation_unit ou JOIN observation_unit_external_references ouex ON ou.id = ouex.observation_unit_entity_id JOIN external_reference ex ON ouex.external_references_id = ex.id - JOIN levels_and_parent_ou_bi_id ON ex.external_reference_id = levels_and_parent_ou_bi_id.parent_ou_id + JOIN levels_and_parent_ou_bi_id ON ex.external_reference_id = levels_and_parent_ou_bi_id.parent_bi_ou_id ) UPDATE observation_unit_level SET level_code = regexp_replace( From 56c982163159f85b58f3d90b8e82ec43af5c2d49 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Wed, 22 Jul 2026 16:33:19 -0400 Subject: [PATCH 11/26] Migrate sample bi ouIds to brapi ouIds --- .../V006_002__migrate_sample_ou_ids.sql | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/main/resources/db/migration/V006_002__migrate_sample_ou_ids.sql diff --git a/src/main/resources/db/migration/V006_002__migrate_sample_ou_ids.sql b/src/main/resources/db/migration/V006_002__migrate_sample_ou_ids.sql new file mode 100644 index 00000000..e944c439 --- /dev/null +++ b/src/main/resources/db/migration/V006_002__migrate_sample_ou_ids.sql @@ -0,0 +1,27 @@ +WITH + samples_and_bi_ou_ids AS ( +-- Look for samples that have obsUnitIDs and keep ids in hand for next query + SELECT + id as sample_id, + additional_info ->> 'obsUnitID' AS bi_ou_id +FROM sample +WHERE additional_info ? 'obsUnitID' + ), + ou_ids_matched_on_samples AS ( +-- Now match the bi-generated exref ou ids to ex refs ids, and keep samples ids for matching in update +SELECT + ou.id AS ou_id, + samples_and_bi_ou_ids.sample_id +FROM observation_unit ou + JOIN observation_unit_external_references ouex ON ou.id = ouex.observation_unit_entity_id + JOIN external_reference ex ON ouex.external_references_id = ex.id + JOIN samples_and_bi_ou_ids ON ex.external_reference_id = samples_and_bi_ou_ids.bi_ou_id + ) +UPDATE sample +SET additional_info = jsonb_set( + additional_info, + '{obsUnitID}', + to_jsonb(mos.ou_id) + ) + FROM ou_ids_matched_on_samples mos +WHERE id = mos.sample_id \ No newline at end of file From e079626f178592c6c270a8160dde5f23dfd1ec68 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Fri, 24 Jul 2026 12:54:11 -0400 Subject: [PATCH 12/26] Add back in distinct to general search build query - This was added for a previous implementation of filter/sorting additional info data that was required by grouping. This is not needed anymore considering now we are using a jpa/hibernate way to grab this data using the entity name for the fields --- .../test/BrAPITestServer/service/SearchQueryBuilder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java index 61ca21f8..007249e2 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -23,8 +23,8 @@ public class SearchQueryBuilder { private Class clazz; public SearchQueryBuilder(Class clazz) { - this.selectClause = "SELECT entity FROM " + clazz.getSimpleName() + " entity "; - this.selectOnlyIds = "SELECT entity.id FROM " + clazz.getSimpleName() + " entity "; + this.selectClause = "SELECT distinct entity FROM " + clazz.getSimpleName() + " entity "; + this.selectOnlyIds = "SELECT distinct entity.id FROM " + clazz.getSimpleName() + " entity "; this.whereClause = "WHERE 1=1 "; this.defaultSort = " ORDER BY entity.id ASC "; this.sortClause = ""; From 647b44fad90ed5cea67415290881a2879799c00d Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Mon, 27 Jul 2026 17:43:22 -0400 Subject: [PATCH 13/26] Implement data types for filtering - Filtering previously was failing on ID/UUID types because of the use of lower and like on UUID type. Supported column name filtering needed types then to differentiate which sql code would be generated based off of the data type of the column. More data types can be supported as needed --- .../java/io/swagger/model/SearchRequest.java | 30 ++++++++-------- .../model/core/StudySearchRequest.java | 30 ++++++++-------- .../model/core/TrialSearchRequest.java | 24 +++++++------ .../model/dto/EntityColumnNameAndType.java | 28 +++++++++++++++ .../BrAPITestServer/model/dto/EntityType.java | 8 +++++ .../service/SearchQueryBuilder.java | 35 ++++++++++++++++--- .../service/core/StudyService.java | 2 +- .../service/core/TrialService.java | 4 +-- 8 files changed, 113 insertions(+), 48 deletions(-) create mode 100644 src/main/java/org/brapi/test/BrAPITestServer/model/dto/EntityColumnNameAndType.java create mode 100644 src/main/java/org/brapi/test/BrAPITestServer/model/dto/EntityType.java diff --git a/src/main/java/io/swagger/model/SearchRequest.java b/src/main/java/io/swagger/model/SearchRequest.java index 828cd69c..aaad3b9a 100644 --- a/src/main/java/io/swagger/model/SearchRequest.java +++ b/src/main/java/io/swagger/model/SearchRequest.java @@ -2,9 +2,10 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; import io.swagger.model.sort.SortBy; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; +import org.brapi.test.BrAPITestServer.model.dto.EntityColumnNameAndType; +import org.brapi.test.BrAPITestServer.model.dto.EntityType; import org.springframework.http.HttpStatus; import java.util.ArrayList; @@ -149,7 +150,7 @@ public void setFilterBy(List filterBy) throws BrAPIServerException { return; } - Map allowedSortFilterNames = getSortFilterEntityColumnNamesByRequestName(); + Map allowedSortFilterNames = getEntityColAndTypeBySubmittedNameMap(); for (FilterBy filterByItem : filterBy) { @@ -161,15 +162,17 @@ public void setFilterBy(List filterBy) throws BrAPIServerException { throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "value attribute not provided in element of filterBy list."); } - String filterColumnEntityName = getSortFilterEntityColumnNamesByRequestName().get(filterByItem.getFilterOn()); + EntityColumnNameAndType entityColumnNameAndType = allowedSortFilterNames.get(filterByItem.getFilterOn()); - if (filterColumnEntityName == null) { + if (entityColumnNameAndType == null) { throw new BrAPIServerException(HttpStatus.BAD_REQUEST, String.format("Supplied filterColumn [%s] not available in allowed names [%s]", filterByItem.getFilterOn(), allowedSortFilterNames.keySet()) ); - } else { - // Remap suppliedFilterColumn to actual entity name supplied by mapper - filterByItem.setFilterOn(filterColumnEntityName); + } + + if (entityColumnNameAndType.getEntityType() == EntityType.BOOLEAN) { + // TODO: Add support for this when it becomes relevant for BI or when there is time. + throw new BrAPIServerException(HttpStatus.BAD_REQUEST, String.format("Filtering not implemented for column name [%s] with associated data type [%s]", entityColumnNameAndType.getEntityColumnName(), EntityType.BOOLEAN)); } } this.filterBy = filterBy; @@ -185,29 +188,26 @@ public void setSortBy(List sortBy) throws BrAPIServerException { return; } - Map allowedSortFilterNames = getSortFilterEntityColumnNamesByRequestName(); + Map allowedSortFilterNames = getEntityColAndTypeBySubmittedNameMap(); for (SortBy sortByItem : sortBy) { if (sortByItem.getSortedOn() == null || sortByItem.getSortedOn().isEmpty()) { throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "sortedOn attribute not provided in element of sortBy list"); } - String sortColumnEntityName = getSortFilterEntityColumnNamesByRequestName().get(sortByItem.getSortedOn()); + EntityColumnNameAndType sortColumnEntityNameAndType = getEntityColAndTypeBySubmittedNameMap().get(sortByItem.getSortedOn()); - if (sortColumnEntityName == null) { + if (sortColumnEntityNameAndType == null) { throw new BrAPIServerException(HttpStatus.BAD_REQUEST, String.format("Supplied sortColumn [%s] not available in allowed names [%s]", sortByItem.getSortedOn(), allowedSortFilterNames.keySet()) ); - } else { - // Remap suppliedFilterColumn to actual entity name supplied by mapper - sortByItem.setSortedOn(sortColumnEntityName); } } this.sortBy = sortBy; } - public Map getSortFilterEntityColumnNamesByRequestName() { - throw new UnsupportedOperationException(String.format("Sort/Filtering not implemented for %s", this.getClass().getSimpleName())); + public Map getEntityColAndTypeBySubmittedNameMap() throws BrAPIServerException { + throw new BrAPIServerException(HttpStatus.BAD_REQUEST, String.format("Sort/Filtering not implemented for %s", this.getClass().getSimpleName())); } } diff --git a/src/main/java/io/swagger/model/core/StudySearchRequest.java b/src/main/java/io/swagger/model/core/StudySearchRequest.java index 78ed90d0..94e760ff 100644 --- a/src/main/java/io/swagger/model/core/StudySearchRequest.java +++ b/src/main/java/io/swagger/model/core/StudySearchRequest.java @@ -6,6 +6,8 @@ import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.model.SearchRequest; +import org.brapi.test.BrAPITestServer.model.dto.EntityColumnNameAndType; +import org.brapi.test.BrAPITestServer.model.dto.EntityType; import java.util.ArrayList; import java.util.List; @@ -13,20 +15,20 @@ public class StudySearchRequest extends SearchRequest { // Key - allowed sort or field filter name for this entity - // Value = entity field name that represents the submitted field. Used later on in query building. - private static final Map ALLOWED_SORT_AND_FILTER_FIELDS = + // Value = entity field name and type that represents the submitted field. Used later on in query building. + private static final Map ALLOWED_SORT_AND_FILTER_FIELDS = Map.ofEntries( - Map.entry("germplasmDbId", "*obsunit.germplasm.id"), - Map.entry("locationDbId", "location.id"), - Map.entry("observationVariableDbId", "*observation.observationVariable.id"), - Map.entry("programDbId", "trial.program.id"), - Map.entry("programName", "trial.program.name"), - Map.entry("seasonDbId", "*season.id"), - Map.entry("studyDbId", "id"), - Map.entry("studyLocation", "location.id"), - Map.entry("trialDbId", "trial.id"), - Map.entry("studyType", "studyName"), - Map.entry("studyName", "studyName") + Map.entry("germplasmDbId", new EntityColumnNameAndType("*obsunit.germplasm.id", EntityType.UUID)), + Map.entry("locationDbId", new EntityColumnNameAndType("location.id", EntityType.UUID)), + Map.entry("observationVariableDbId", new EntityColumnNameAndType("*observation.observationVariable.id", EntityType.UUID)), + Map.entry("programDbId", new EntityColumnNameAndType("trial.program.id", EntityType.UUID)), + Map.entry("programName", new EntityColumnNameAndType("trial.program.name", EntityType.TEXT)), + Map.entry("seasonDbId", new EntityColumnNameAndType("*season.id", EntityType.UUID)), + Map.entry("studyDbId", new EntityColumnNameAndType("id", EntityType.UUID)), + Map.entry("studyLocation", new EntityColumnNameAndType("location.id", EntityType.UUID)), + Map.entry("trialDbId", new EntityColumnNameAndType("trial.id", EntityType.UUID)), + Map.entry("studyType", new EntityColumnNameAndType("studyName", EntityType.TEXT)), + Map.entry("studyName", new EntityColumnNameAndType("studyName", EntityType.TEXT)) ); @JsonProperty("commonCropNames") @@ -579,7 +581,7 @@ public Integer getTotalParameterCount() { } @Override - public Map getSortFilterEntityColumnNamesByRequestName() { + public Map getEntityColAndTypeBySubmittedNameMap() { return ALLOWED_SORT_AND_FILTER_FIELDS; } } diff --git a/src/main/java/io/swagger/model/core/TrialSearchRequest.java b/src/main/java/io/swagger/model/core/TrialSearchRequest.java index ce24b1f7..a4a2feaf 100644 --- a/src/main/java/io/swagger/model/core/TrialSearchRequest.java +++ b/src/main/java/io/swagger/model/core/TrialSearchRequest.java @@ -4,23 +4,25 @@ import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.model.SearchRequest; +import org.brapi.test.BrAPITestServer.model.dto.EntityColumnNameAndType; +import org.brapi.test.BrAPITestServer.model.dto.EntityType; import java.time.LocalDate; public class TrialSearchRequest extends SearchRequest { // Key - allowed sort or field filter name for this entity - // Value = entity field name that represents the submitted field. Used later on in query building. - private static final Map ALLOWED_SORT_AND_FILTER_FIELDS = + // Value = entity field name and type that represents the submitted field. Used later on in query building. + private static final Map ALLOWED_SORT_AND_FILTER_FIELDS = Map.of( - "trialName", "trialName", - "createdDate", "createdDate", - "createdBy", "createdBy", - "trialDbId", "id", - "programDbId","program.id", - "startDate", "startDate", - "endDate", "endDate", - "active", "active" + "trialName", new EntityColumnNameAndType("trialName", EntityType.TEXT), + "createdDate", new EntityColumnNameAndType("createdDate", EntityType.TEXT), + "createdBy", new EntityColumnNameAndType("createdBy", EntityType.TEXT), + "trialDbId", new EntityColumnNameAndType("id", EntityType.UUID), + "programDbId",new EntityColumnNameAndType("program.id", EntityType.UUID), + "startDate", new EntityColumnNameAndType("startDate", EntityType.TEXT), + "endDate", new EntityColumnNameAndType("endDate", EntityType.TEXT), + "active", new EntityColumnNameAndType("active", EntityType.BOOLEAN) ); @JsonProperty("commonCropNames") @@ -440,7 +442,7 @@ public Integer getTotalParameterCount() { } @Override - public Map getSortFilterEntityColumnNamesByRequestName() { + public Map getEntityColAndTypeBySubmittedNameMap() { return ALLOWED_SORT_AND_FILTER_FIELDS; } } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/model/dto/EntityColumnNameAndType.java b/src/main/java/org/brapi/test/BrAPITestServer/model/dto/EntityColumnNameAndType.java new file mode 100644 index 00000000..71f4a9f2 --- /dev/null +++ b/src/main/java/org/brapi/test/BrAPITestServer/model/dto/EntityColumnNameAndType.java @@ -0,0 +1,28 @@ +package org.brapi.test.BrAPITestServer.model.dto; + +/** + * This class is used to map an entity's column name to the SQL type of lookup that should be completed for filter searches. + * The entityColumnName is also used for sorts as well. + */ +public class EntityColumnNameAndType { + String entityColumnName; + EntityType entityType; + + public EntityColumnNameAndType(String entityColumnName, EntityType entityType) { + this.entityColumnName = entityColumnName; + this.entityType = entityType; + } + + public String getEntityColumnName() { + return entityColumnName; + } + + public EntityType getEntityType() { + return entityType; + } + + @Override + public String toString() { + return this.entityColumnName; + } +} diff --git a/src/main/java/org/brapi/test/BrAPITestServer/model/dto/EntityType.java b/src/main/java/org/brapi/test/BrAPITestServer/model/dto/EntityType.java new file mode 100644 index 00000000..e69aa988 --- /dev/null +++ b/src/main/java/org/brapi/test/BrAPITestServer/model/dto/EntityType.java @@ -0,0 +1,8 @@ +package org.brapi.test.BrAPITestServer.model.dto; + +public enum EntityType { + TEXT, + UUID, + BOOLEAN + // Add any other entity data types we should filter different in SearchQueryBuilder here +} diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java index 007249e2..c7e7da92 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -11,6 +11,8 @@ import io.swagger.model.sort.SortBy; import io.swagger.model.sort.SortOrder; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; +import org.brapi.test.BrAPITestServer.model.dto.EntityColumnNameAndType; +import org.brapi.test.BrAPITestServer.model.dto.EntityType; public class SearchQueryBuilder { @@ -146,6 +148,16 @@ public SearchQueryBuilder appendLike(String like, String columnName) { return this; } + public SearchQueryBuilder appendLikeIDs(String like, String columnName) { + String paramName = paramFilter(columnName); + + if (like != null) { + this.whereClause += "AND cast(" + entityPrefix(columnName) + " as String) LIKE :" + paramName + " "; + this.params.put(paramName, "%" + like + "%"); + } + return this; + } + public > SearchQueryBuilder appendEnum(E enumVal, String columnName) { String paramName = paramFilter(columnName); if (enumVal != null) { @@ -329,20 +341,24 @@ private String paramFilter(String param) { } /** - * Takes a list of SortBy options that should typically come in a searchRequest. + * Takes a list of SortBy options that should typically come in a searchRequest, along with a map of the validated + * columns names. * Applies the entries in the list to sort the SearchQuery. * * A SortBy has * - A column name * - An order (DESC, ASC) */ - public SearchQueryBuilder sortBy(List sortBy) throws BrAPIServerException { + public SearchQueryBuilder sortBy(List sortBy, Map entityColAndTypeBySubmittedName) throws BrAPIServerException { if (sortBy == null || sortBy.isEmpty()) { return this; } for (SortBy sort : sortBy) { + // At this point, the submitted sortBy name has been verified to be in entityColAndTypeBySubmittedName + sort.setSortedOn(entityColAndTypeBySubmittedName.get(sort.getSortedOn()).getEntityColumnName()); + if (sortBy.getFirst().equals(sort)) { this.sortClause += " ORDER BY "; buildSort(sort); @@ -360,13 +376,15 @@ private void buildSort(SortBy sort) { } /** - * Takes a list of FilterBy options that should typically come in a searchRequest. + * Takes a list of FilterBy options that should typically come in a searchRequest, along with a map of the validated + * columns names and the data type they represent for accurate filtering on different data types. * Applies the entries in the list to filter the SearchQuery. * * A FilterBy has * - A column name + * - A value which the column name should be filtered on */ - public SearchQueryBuilder filterBy(List filterBy) throws BrAPIServerException { + public SearchQueryBuilder filterBy(List filterBy, Map entityColAndTypeBySubmittedName) throws BrAPIServerException { SearchQueryBuilder searchQuery = this; if (filterBy == null || filterBy.isEmpty()) { @@ -374,7 +392,14 @@ public SearchQueryBuilder filterBy(List filterBy) throws BrAPIServe } for (FilterBy filter : filterBy) { - searchQuery = appendLike(filter.getValue().toLowerCase(), filter.getFilterOn()); + // At this point, the submitted filterBy column name has been verified to be in entityColAndTypeBySubmittedName + EntityColumnNameAndType entityColumnNameAndType = entityColAndTypeBySubmittedName.get(filter.getFilterOn()); + + if (entityColumnNameAndType.getEntityType() == EntityType.TEXT) { + searchQuery = appendLike(filter.getValue().toLowerCase(), entityColumnNameAndType.getEntityColumnName()); + } else if (entityColumnNameAndType.getEntityType() == EntityType.UUID) { + searchQuery = appendLikeIDs(filter.getValue(), entityColumnNameAndType.getEntityColumnName()); + } } return searchQuery; diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/core/StudyService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/core/StudyService.java index e5611ab7..602234a2 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/core/StudyService.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/core/StudyService.java @@ -159,7 +159,7 @@ public List findStudies(StudySearchRequest request, Metadata metaData) .appendList(request.getStudyDbIds(), "id").appendList(request.getStudyNames(), "studyName") .appendList(request.getStudyPUIs(), "studyPUI").appendList(request.getStudyTypes(), "studyType") .appendList(request.getTrialDbIds(), "trial.id").appendList(request.getTrialNames(), "trial.trialName") - .sortBy(request.getSortByElements()); + .sortBy(request.getSortByElements(), request.getEntityColAndTypeBySubmittedNameMap()); Page studiesPage = studyRepository.findAllBySearchAndPaginate(searchQuery, pageReq); PagingUtility.calculateMetaData(metaData, studiesPage); diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/core/TrialService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/core/TrialService.java index 21146a30..9aa723a9 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/core/TrialService.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/core/TrialService.java @@ -127,8 +127,8 @@ public List findTrials(@Valid TrialSearchRequest request, Metadata metada .appendList(request.getStudyNames(), "*study.studyName").appendList(request.getTrialDbIds(), "id") .appendList(request.getTrialNames(), "trialName") .appendDateRange(request.getSearchDateRangeStart(), request.getSearchDateRangeEnd(), "startDate") - .sortBy(request.getSortByElements()) - .filterBy(request.getFilterBy()); + .sortBy(request.getSortByElements(), request.getEntityColAndTypeBySubmittedNameMap()) + .filterBy(request.getFilterBy(), request.getEntityColAndTypeBySubmittedNameMap()); Page trialsPage = trialRepository.findAllBySearchAndPaginate(searchQuery, pageReq); PagingUtility.calculateMetaData(metadata, trialsPage); From b095c97517c11ca0a1758c9cd5a87eda0e7a619d Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Mon, 3 Aug 2026 14:37:43 -0400 Subject: [PATCH 14/26] Add code for filtering on one to many relationship entity collections --- .../model/core/TrialSearchRequest.java | 4 +- .../service/SearchQueryBuilder.java | 37 +++++++++++++++---- .../service/core/TrialService.java | 10 ++--- 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/src/main/java/io/swagger/model/core/TrialSearchRequest.java b/src/main/java/io/swagger/model/core/TrialSearchRequest.java index a4a2feaf..2760e5d5 100644 --- a/src/main/java/io/swagger/model/core/TrialSearchRequest.java +++ b/src/main/java/io/swagger/model/core/TrialSearchRequest.java @@ -22,7 +22,9 @@ public class TrialSearchRequest extends SearchRequest { "programDbId",new EntityColumnNameAndType("program.id", EntityType.UUID), "startDate", new EntityColumnNameAndType("startDate", EntityType.TEXT), "endDate", new EntityColumnNameAndType("endDate", EntityType.TEXT), - "active", new EntityColumnNameAndType("active", EntityType.BOOLEAN) + "active", new EntityColumnNameAndType("active", EntityType.BOOLEAN), + "programName", new EntityColumnNameAndType("program.name", EntityType.TEXT), + "locationDbId", new EntityColumnNameAndType("*studies.location.id", EntityType.UUID) ); @JsonProperty("commonCropNames") diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java index c7e7da92..e93310bc 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -9,10 +9,10 @@ import io.swagger.model.FilterBy; import io.swagger.model.GeoJSONSearchArea; import io.swagger.model.sort.SortBy; -import io.swagger.model.sort.SortOrder; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; import org.brapi.test.BrAPITestServer.model.dto.EntityColumnNameAndType; import org.brapi.test.BrAPITestServer.model.dto.EntityType; +import org.springframework.http.HttpStatus; public class SearchQueryBuilder { @@ -330,10 +330,6 @@ private String entityPrefix(String field) { } } - private String addInfoPrefix(String field) { - return "function('jsonb_extract_path_text', entity.additionalInfo, '" + field + "' ) "; - } - private String paramFilter(String param) { if (param == null) return ""; @@ -357,7 +353,15 @@ public SearchQueryBuilder sortBy(List sortBy, Map filterBy(List filterBy, Map filterBy(List filterBy, Map findTrials(@Valid TrialSearchRequest request, Metadata metada searchQuery = searchQuery.join("contacts", "contact"); } if (request.getStudyDbIds() != null || request.getStudyNames() != null) { - searchQuery = searchQuery.join("studies", "study"); + searchQuery = searchQuery.join("studies", "studies"); } searchQuery = searchQuery.withExRefs(request.getExternalReferenceIDs(), request.getExternalReferenceSources()) .appendList(request.getCommonCropNames(), "crop.cropName") .appendList(request.getContactDbIds(), "*contact.id") - .appendList(request.getLocationDbIds(), "*study.location.id") - .appendList(request.getLocationNames(), "*study.location.locationName") + .appendList(request.getLocationDbIds(), "*studies.location.id") + .appendList(request.getLocationNames(), "*studies.location.locationName") .appendList(request.getProgramDbIds(), "program.id") - .appendList(request.getProgramNames(), "program.name").appendList(request.getStudyDbIds(), "*study.id") - .appendList(request.getStudyNames(), "*study.studyName").appendList(request.getTrialDbIds(), "id") + .appendList(request.getProgramNames(), "program.name").appendList(request.getStudyDbIds(), "*studies.id") + .appendList(request.getStudyNames(), "*studies.studyName").appendList(request.getTrialDbIds(), "id") .appendList(request.getTrialNames(), "trialName") .appendDateRange(request.getSearchDateRangeStart(), request.getSearchDateRangeEnd(), "startDate") .sortBy(request.getSortByElements(), request.getEntityColAndTypeBySubmittedNameMap()) From 05fb0822f6b9174e451231b16a9fda59374b3645 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Tue, 4 Aug 2026 14:28:31 -0400 Subject: [PATCH 15/26] Fix issue with sorting for GET study/trial endpoints --- src/main/java/io/swagger/model/core/StudySearchRequest.java | 4 ++-- src/main/java/io/swagger/model/sort/SortOrder.java | 2 +- .../test/BrAPITestServer/service/SearchQueryBuilder.java | 2 -- .../brapi/test/BrAPITestServer/service/core/StudyService.java | 2 +- .../brapi/test/BrAPITestServer/service/core/TrialService.java | 2 +- 5 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/main/java/io/swagger/model/core/StudySearchRequest.java b/src/main/java/io/swagger/model/core/StudySearchRequest.java index 94e760ff..26a134f0 100644 --- a/src/main/java/io/swagger/model/core/StudySearchRequest.java +++ b/src/main/java/io/swagger/model/core/StudySearchRequest.java @@ -21,8 +21,8 @@ public class StudySearchRequest extends SearchRequest { Map.entry("germplasmDbId", new EntityColumnNameAndType("*obsunit.germplasm.id", EntityType.UUID)), Map.entry("locationDbId", new EntityColumnNameAndType("location.id", EntityType.UUID)), Map.entry("observationVariableDbId", new EntityColumnNameAndType("*observation.observationVariable.id", EntityType.UUID)), - Map.entry("programDbId", new EntityColumnNameAndType("trial.program.id", EntityType.UUID)), - Map.entry("programName", new EntityColumnNameAndType("trial.program.name", EntityType.TEXT)), + Map.entry("programDbId", new EntityColumnNameAndType("program.id", EntityType.UUID)), + Map.entry("programName", new EntityColumnNameAndType("program.name", EntityType.TEXT)), Map.entry("seasonDbId", new EntityColumnNameAndType("*season.id", EntityType.UUID)), Map.entry("studyDbId", new EntityColumnNameAndType("id", EntityType.UUID)), Map.entry("studyLocation", new EntityColumnNameAndType("location.id", EntityType.UUID)), diff --git a/src/main/java/io/swagger/model/sort/SortOrder.java b/src/main/java/io/swagger/model/sort/SortOrder.java index 4cc9d5aa..b8a85ebf 100644 --- a/src/main/java/io/swagger/model/sort/SortOrder.java +++ b/src/main/java/io/swagger/model/sort/SortOrder.java @@ -26,6 +26,6 @@ public static SortOrder fromValue(String text) { return b; } } - return null; + return ASC; } } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java index e93310bc..f13179e7 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -361,8 +361,6 @@ public SearchQueryBuilder sortBy(List sortBy, Map findStudies(String commonCropName, String studyType, String p if (active != null) request.setActive(active); if (sortBy != null) { - SortBy sortByElement = new SortBy(sortBy, SortOrder.valueOf(sortOrder)); + SortBy sortByElement = new SortBy(sortBy, SortOrder.fromValue(sortOrder)); request.setSortBy(List.of(sortByElement)); } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/core/TrialService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/core/TrialService.java index 601c4606..21c30c65 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/core/TrialService.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/core/TrialService.java @@ -97,7 +97,7 @@ public List findTrials(@Valid String commonCropName, @Valid String contac if (searchDateRangeEnd != null) request.setSearchDateRangeEnd(searchDateRangeEnd); if (sortBy != null) { - SortBy querySortBy = new SortBy(sortBy, SortOrder.valueOf(sortOrder)); + SortBy querySortBy = new SortBy(sortBy, SortOrder.fromValue(sortOrder)); request.setSortBy(List.of(querySortBy)); } request.addExternalReferenceItem(externalReferenceId, externalReferenceID, externalReferenceSource); From b0bf8baaafeadacaa0b49bf21238b64fb15e1bae Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Thu, 6 Aug 2026 18:15:36 -0400 Subject: [PATCH 16/26] Fix issues with using distinct in sorting --- .../model/core/StudySearchRequest.java | 4 +- .../service/SearchQueryBuilder.java | 52 +++++++++++++++---- .../service/germ/GermplasmService.java | 1 + 3 files changed, 46 insertions(+), 11 deletions(-) diff --git a/src/main/java/io/swagger/model/core/StudySearchRequest.java b/src/main/java/io/swagger/model/core/StudySearchRequest.java index 26a134f0..94e760ff 100644 --- a/src/main/java/io/swagger/model/core/StudySearchRequest.java +++ b/src/main/java/io/swagger/model/core/StudySearchRequest.java @@ -21,8 +21,8 @@ public class StudySearchRequest extends SearchRequest { Map.entry("germplasmDbId", new EntityColumnNameAndType("*obsunit.germplasm.id", EntityType.UUID)), Map.entry("locationDbId", new EntityColumnNameAndType("location.id", EntityType.UUID)), Map.entry("observationVariableDbId", new EntityColumnNameAndType("*observation.observationVariable.id", EntityType.UUID)), - Map.entry("programDbId", new EntityColumnNameAndType("program.id", EntityType.UUID)), - Map.entry("programName", new EntityColumnNameAndType("program.name", EntityType.TEXT)), + Map.entry("programDbId", new EntityColumnNameAndType("trial.program.id", EntityType.UUID)), + Map.entry("programName", new EntityColumnNameAndType("trial.program.name", EntityType.TEXT)), Map.entry("seasonDbId", new EntityColumnNameAndType("*season.id", EntityType.UUID)), Map.entry("studyDbId", new EntityColumnNameAndType("id", EntityType.UUID)), Map.entry("studyLocation", new EntityColumnNameAndType("location.id", EntityType.UUID)), diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java index f13179e7..cd9e3020 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -22,6 +22,8 @@ public class SearchQueryBuilder { private String defaultSort; private String sortClause; private Map params; + private List joinedTables = new ArrayList<>(); + private List joinedFetchedTables = new ArrayList<>(); private Class clazz; public SearchQueryBuilder(Class clazz) { @@ -283,13 +285,31 @@ public SearchQueryBuilder withExRefs(List exRefIds, List exRe } public SearchQueryBuilder join(String join, String name) { - this.selectClause += "JOIN " + entityPrefix(join) + " " + paramFilter(name) + " "; - this.selectOnlyIds += "JOIN " + entityPrefix(join) + " " + paramFilter(name) + " "; + + if (!this.joinedTables.contains(join) && !this.joinedFetchedTables.contains(join)) { + this.selectClause += "JOIN " + entityPrefix(join) + " " + paramFilter(name) + " "; + this.selectOnlyIds += "JOIN " + entityPrefix(join) + " " + paramFilter(name) + " "; + this.joinedTables.add(join); + } return this; } public SearchQueryBuilder leftJoinFetch(String join, String name) { - this.selectClause += generateLeftJoinFetch(join, name); + return leftJoinFetch(join, name, false); + } + + public SearchQueryBuilder leftJoinFetch(String join, String name, boolean overrideExistingJoin) { + + if (this.joinedTables.contains(join) && overrideExistingJoin) { + // Override existing normal join with join fetch if it already exists in a query + // This override does not change the alias to the name provided, and assumes further usages will use the same alias. + this.selectClause = this.selectClause.replace("JOIN " + entityPrefix(join), "LEFT JOIN FETCH " + entityPrefix(join)); + this.joinedFetchedTables.add(join); + this.joinedTables.remove(join); + } else if (!this.joinedFetchedTables.contains(join)) { + this.selectClause += generateLeftJoinFetch(join, name); + this.joinedFetchedTables.add(join); + } return this; } @@ -305,6 +325,9 @@ public SearchQueryBuilder removeAndReplaceLeftJoinFetch(String join, this.selectClause = this.selectClause.replace(generateLeftJoinFetch(existingJoin, existingName), generateLeftJoinFetch(join, name)); + this.joinedFetchedTables.remove(existingJoin); + this.joinedFetchedTables.add(join); + return this; } @@ -315,6 +338,7 @@ public SearchQueryBuilder removeAndReplaceLeftJoinFetch(String join, public SearchQueryBuilder removeLeftJoinFetch(String join, String name) { this.selectClause = this.selectClause.replace(generateLeftJoinFetch(join, name), ""); + this.joinedFetchedTables.remove(join); return this; } @@ -355,11 +379,24 @@ public SearchQueryBuilder sortBy(List sortBy, Map 2) { + // TODO: Implement this if it becomes a requirement + throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "Sorting on a table greater than one level from primary entity not allowed"); + } + + if (split.length == 2 && !split[1].equals("id")) { + leftJoinFetch(split[0], split[0], true); + } + + sort.setSortedOn(entityColName); if (sortBy.getFirst().equals(sort)) { this.sortClause += " ORDER BY "; @@ -419,9 +456,6 @@ public SearchQueryBuilder filterBy(List filterBy, Map findGermplasmEntities(@Valid GermplasmSearchRequest return germs; } + // TODO: Investigate/consider removing this code. It is largely unused by DeltaBreed now, as paginating is the least memory exhaustive option. [BI-3020] public List findGermplasmEntitiesWithoutPaging(@Valid GermplasmSearchRequest request) { SearchQueryBuilder searchQuery = buildGermplasmSearchQuery(request); From 22736ae07cb6a444569455be53d42637591e5ca6 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Fri, 7 Aug 2026 12:14:29 -0400 Subject: [PATCH 17/26] Add Pattern to end of filter params in SearchQueryBuilder - This prevents collisions with existing parameter lookups --- .../BrAPITestServer/service/SearchQueryBuilder.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java index cd9e3020..d18a30a0 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -141,7 +141,7 @@ public SearchQueryBuilder appendSingle(UUID single, String columnName) { } public SearchQueryBuilder appendLike(String like, String columnName) { - String paramName = paramFilter(columnName); + String paramName = paramFilterPattern(columnName); if (like != null) { this.whereClause += "AND lower(" + entityPrefix(columnName) + ") LIKE :" + paramName + " "; @@ -151,7 +151,7 @@ public SearchQueryBuilder appendLike(String like, String columnName) { } public SearchQueryBuilder appendLikeIDs(String like, String columnName) { - String paramName = paramFilter(columnName); + String paramName = paramFilterPattern(columnName); if (like != null) { this.whereClause += "AND cast(" + entityPrefix(columnName) + " as String) LIKE :" + paramName + " "; @@ -360,6 +360,12 @@ private String paramFilter(String param) { return param.replace('.', '_').replace('*', '_'); } + private String paramFilterPattern(String param) { + if (param == null) + return ""; + return param.replace('.', '_').replace('*', '_') + "Pattern"; + } + /** * Takes a list of SortBy options that should typically come in a searchRequest, along with a map of the validated * columns names. From 2bd1d4799929554c84d7625697deb36c9c90434a Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Mon, 10 Aug 2026 17:35:13 -0400 Subject: [PATCH 18/26] Create migration script, add programDbId to entity and model --- .../model/germ/GermplasmNewRequest.java | 17 ++++++++++++-- .../model/entity/germ/GermplasmEntity.java | 11 ++++++++++ ...dd_program_id_to_germplasm_and_migrate.sql | 22 +++++++++++++++++++ 3 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 src/main/resources/db/migration/V006_003__add_program_id_to_germplasm_and_migrate.sql diff --git a/src/main/java/io/swagger/model/germ/GermplasmNewRequest.java b/src/main/java/io/swagger/model/germ/GermplasmNewRequest.java index 253783e7..b52c5f09 100644 --- a/src/main/java/io/swagger/model/germ/GermplasmNewRequest.java +++ b/src/main/java/io/swagger/model/germ/GermplasmNewRequest.java @@ -97,6 +97,9 @@ public class GermplasmNewRequest extends BrAPIDataModel { @JsonProperty("taxonIds") private List taxonIds = null; + @JsonProperty("programDbId") + private String programDbId = null; + public String getBreedingMethodName() { return breedingMethodName; } @@ -510,6 +513,14 @@ public void setTaxonIds(List taxonIds) { this.taxonIds = taxonIds; } + public String getProgramDbId() { + return programDbId; + } + + public void setProgramDbId(String programDbId) { + this.programDbId = programDbId; + } + @Override public boolean equals(java.lang.Object o) { if (this == o) { @@ -550,7 +561,8 @@ public boolean equals(java.lang.Object o) { && Objects.equals(this.subtaxa, germplasmNewRequest.subtaxa) && Objects.equals(this.subtaxaAuthority, germplasmNewRequest.subtaxaAuthority) && Objects.equals(this.synonyms, germplasmNewRequest.synonyms) - && Objects.equals(this.taxonIds, germplasmNewRequest.taxonIds); + && Objects.equals(this.taxonIds, germplasmNewRequest.taxonIds) + && Objects.equals(this.programDbId, germplasmNewRequest.programDbId); } @Override @@ -560,7 +572,7 @@ public int hashCode() { countryOfOriginCode, defaultDisplayName, documentationURL, donors, externalReferences, genus, germplasmName, germplasmOrigin, germplasmPUI, germplasmPreprocessing, instituteCode, instituteName, pedigree, seedSource, seedSourceDescription, species, speciesAuthority, storageTypes, subtaxa, - subtaxaAuthority, synonyms, taxonIds); + subtaxaAuthority, synonyms, taxonIds, programDbId); } @Override @@ -600,6 +612,7 @@ public String toString() { sb.append(" subtaxaAuthority: ").append(toIndentedString(subtaxaAuthority)).append("\n"); sb.append(" synonyms: ").append(toIndentedString(synonyms)).append("\n"); sb.append(" taxonIds: ").append(toIndentedString(taxonIds)).append("\n"); + sb.append(" programDbId: ").append(toIndentedString(programDbId)).append("\n"); sb.append("}"); return sb.toString(); } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/model/entity/germ/GermplasmEntity.java b/src/main/java/org/brapi/test/BrAPITestServer/model/entity/germ/GermplasmEntity.java index 1c5192db..8b464b85 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/model/entity/germ/GermplasmEntity.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/model/entity/germ/GermplasmEntity.java @@ -9,6 +9,7 @@ import org.brapi.test.BrAPITestServer.model.entity.BrAPIPrimaryEntity; import org.brapi.test.BrAPITestServer.model.entity.SearchRequestEntity; import org.brapi.test.BrAPITestServer.model.entity.core.CropEntity; +import org.brapi.test.BrAPITestServer.model.entity.core.ProgramEntity; import org.brapi.test.BrAPITestServer.model.entity.germ.GermplasmInstituteEntity.InstituteTypeEnum; import org.brapi.test.BrAPITestServer.model.entity.pheno.ObservationUnitEntity; import org.brapi.test.BrAPITestServer.model.entity.pheno.TaxonEntity; @@ -96,6 +97,8 @@ public class GermplasmEntity extends BrAPIPrimaryEntity { private List typeOfGermplasmStorageCode; @Column(name = "soft_deleted") private boolean softDeleted; + @ManyToOne(cascade = CascadeType.DETACH, fetch = FetchType.LAZY) + private ProgramEntity program; public GermplasmInstituteEntity getHostInstitute() { if (getInstitutes() != null) { @@ -368,4 +371,12 @@ public void setTypeOfGermplasmStorageCode(List typeOf public void setSoftDeleted(boolean sofDeleted) { this.softDeleted = sofDeleted; } + public ProgramEntity getProgram() { + return program; + } + + public void setProgram(ProgramEntity program) { + this.program = program; + } + } diff --git a/src/main/resources/db/migration/V006_003__add_program_id_to_germplasm_and_migrate.sql b/src/main/resources/db/migration/V006_003__add_program_id_to_germplasm_and_migrate.sql new file mode 100644 index 00000000..9f5b2aae --- /dev/null +++ b/src/main/resources/db/migration/V006_003__add_program_id_to_germplasm_and_migrate.sql @@ -0,0 +1,22 @@ +ALTER TABLE germplasm +ADD COLUMN program_id UUID; + +ALTER TABLE germplasm +ADD CONSTRAINT germplasm_program_fk + FOREIGN KEY (program_id) + REFERENCES public.program(id) + ON DELETE CASCADE; + +CREATE INDEX germplasm_program_idx ON germplasm (program_id, id); + +UPDATE germplasm set program_id = pquery.program_id +FROM ( + SELECT g.id AS germplasm_id, p.id AS program_id + from germplasm g + JOIN germplasm_external_references gex ON g.id = gex.germplasm_entity_id + JOIN external_reference ex ON ex.id = gex.external_references_id + JOIN external_reference ex2 ON ex2.external_reference_id = ex.external_reference_id + JOIN program_external_references pex ON pex.external_references_id = ex2.id + JOIN program p ON p.id = pex.program_entity_id +) pquery +where id = pquery.germplasm_id; \ No newline at end of file From 2bad4ff95942ca919f86eaae2406354455798bbe Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Tue, 11 Aug 2026 14:52:38 -0400 Subject: [PATCH 19/26] Support for programId in GermplasmApi --- .../io/swagger/api/germ/GermplasmApi.java | 1 + .../model/germ/GermplasmNewRequest.java | 17 +++++++- .../germ/GermplasmApiController.java | 3 +- .../service/core/ProgramService.java | 31 ++++++++++---- .../service/germ/GermplasmService.java | 42 ++++++++++++++++--- 5 files changed, 78 insertions(+), 16 deletions(-) diff --git a/src/main/java/io/swagger/api/germ/GermplasmApi.java b/src/main/java/io/swagger/api/germ/GermplasmApi.java index 1990ba83..a7f9d959 100644 --- a/src/main/java/io/swagger/api/germ/GermplasmApi.java +++ b/src/main/java/io/swagger/api/germ/GermplasmApi.java @@ -143,6 +143,7 @@ ResponseEntity germplasmGet( @ApiParam(value = "progenyDbId") @Valid @RequestParam(value = "progenyDbId", required = false) String progenyDbId, @ApiParam(value = "commonCropName") @Valid @RequestParam(value = "commonCropName", required = false) String commonCropName, @ApiParam(value = "programDbId") @Valid @RequestParam(value = "programDbId", required = false) String programDbId, + @ApiParam(value = "programName") @Valid @RequestParam(value = "programName", required = false) String programName, @ApiParam(value = "externalReferenceID") @Valid @RequestParam(value = "externalReferenceID", required = false) String externalReferenceID, @ApiParam(value = "externalReferenceId") @Valid @RequestParam(value = "externalReferenceId", required = false) String externalReferenceId, @ApiParam(value = "externalReferenceSource") @Valid @RequestParam(value = "externalReferenceSource", required = false) String externalReferenceSource, diff --git a/src/main/java/io/swagger/model/germ/GermplasmNewRequest.java b/src/main/java/io/swagger/model/germ/GermplasmNewRequest.java index b52c5f09..7317f374 100644 --- a/src/main/java/io/swagger/model/germ/GermplasmNewRequest.java +++ b/src/main/java/io/swagger/model/germ/GermplasmNewRequest.java @@ -100,6 +100,9 @@ public class GermplasmNewRequest extends BrAPIDataModel { @JsonProperty("programDbId") private String programDbId = null; + @JsonProperty("programName") + private String programName = null; + public String getBreedingMethodName() { return breedingMethodName; } @@ -521,6 +524,14 @@ public void setProgramDbId(String programDbId) { this.programDbId = programDbId; } + public String getProgramName() { + return programName; + } + + public void setProgramName(String programName) { + this.programName = programName; + } + @Override public boolean equals(java.lang.Object o) { if (this == o) { @@ -562,7 +573,8 @@ public boolean equals(java.lang.Object o) { && Objects.equals(this.subtaxaAuthority, germplasmNewRequest.subtaxaAuthority) && Objects.equals(this.synonyms, germplasmNewRequest.synonyms) && Objects.equals(this.taxonIds, germplasmNewRequest.taxonIds) - && Objects.equals(this.programDbId, germplasmNewRequest.programDbId); + && Objects.equals(this.programDbId, germplasmNewRequest.programDbId) + && Objects.equals(this.programName, germplasmNewRequest.programName); } @Override @@ -572,7 +584,7 @@ public int hashCode() { countryOfOriginCode, defaultDisplayName, documentationURL, donors, externalReferences, genus, germplasmName, germplasmOrigin, germplasmPUI, germplasmPreprocessing, instituteCode, instituteName, pedigree, seedSource, seedSourceDescription, species, speciesAuthority, storageTypes, subtaxa, - subtaxaAuthority, synonyms, taxonIds, programDbId); + subtaxaAuthority, synonyms, taxonIds, programDbId, programName); } @Override @@ -613,6 +625,7 @@ public String toString() { sb.append(" synonyms: ").append(toIndentedString(synonyms)).append("\n"); sb.append(" taxonIds: ").append(toIndentedString(taxonIds)).append("\n"); sb.append(" programDbId: ").append(toIndentedString(programDbId)).append("\n"); + sb.append(" programName: ").append(toIndentedString(programName)).append("\n"); sb.append("}"); return sb.toString(); } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/controller/germ/GermplasmApiController.java b/src/main/java/org/brapi/test/BrAPITestServer/controller/germ/GermplasmApiController.java index 52e58a1d..dd462c4b 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/controller/germ/GermplasmApiController.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/controller/germ/GermplasmApiController.java @@ -156,6 +156,7 @@ public ResponseEntity germplasmGet( @RequestParam(value = "progenyDbId", required = false) String progenyDbId, @RequestParam(value = "commonCropName", required = false) String commonCropName, @RequestParam(value = "programDbId", required = false) String programDbId, + @RequestParam(value = "programName", required = false) String programName, @RequestParam(value = "externalReferenceID", required = false) String externalReferenceID, @RequestParam(value = "externalReferenceId", required = false) String externalReferenceId, @RequestParam(value = "externalReferenceSource", required = false) String externalReferenceSource, @@ -170,7 +171,7 @@ public ResponseEntity germplasmGet( Metadata metadata = generateMetaDataTemplate(page, pageSize); List data = germplasmService.findGermplasm(germplasmPUI, germplasmDbId, germplasmName, accessionNumber, collection, binomialName, genus, species, trialDbId, studyDbId, synonym, parentDbId, - progenyDbId, commonCropName, programDbId, externalReferenceId, externalReferenceID, + progenyDbId, commonCropName, programDbId, programName, externalReferenceId, externalReferenceID, externalReferenceSource, metadata); return responseOK(new GermplasmListResponse(), new GermplasmListResponseResult(), data, metadata); } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/core/ProgramService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/core/ProgramService.java index 720f731b..9f11a8f7 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/core/ProgramService.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/core/ProgramService.java @@ -1,9 +1,7 @@ package org.brapi.test.BrAPITestServer.service.core; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.UUID; +import java.util.*; +import java.util.stream.Collectors; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerDbIdNotFoundException; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; @@ -76,14 +74,33 @@ public List findPrograms(ProgramSearchRequest request, Metadata metadat return programs; } - public List findByIds(List programDbIds) { - var result = new ArrayList(); + public List findByIds(List programDbIds) throws BrAPIServerException { + List result = new ArrayList<>(); if (programDbIds.isEmpty()) { return result; } - return programRepository.findByIdIn(programDbIds.stream().map(UUID::fromString).toList()); + // Dedup programIds by loading into set + Set programDbIdSet = new HashSet<>(programDbIds); + + result = programRepository.findByIdIn(programDbIds.stream().map(UUID::fromString).toList()); + + if (programDbIdSet.size() != result.size()) { + List dbIdsNotFound = new ArrayList<>(); + + Set foundDbIds = result.stream().map(pe -> pe.getId().toString()).collect(Collectors.toSet()); + + programDbIdSet.forEach(dbId -> { + if (!foundDbIds.contains(dbId)) { + dbIdsNotFound.add(dbId); + } + }); + + throw new BrAPIServerException(HttpStatus.NOT_FOUND, String.format("The following submitted programDbIds were not found in the db: [%s]", dbIdsNotFound)); + } + + return result; } public Program getProgram(String programDbId) throws BrAPIServerException { diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java index 2946fd58..a09abdad 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java @@ -4,6 +4,7 @@ import java.util.*; import java.util.stream.Collectors; +import io.swagger.model.core.ProgramSearchRequest; import io.swagger.model.germ.*; import jakarta.validation.Valid; @@ -12,6 +13,7 @@ import org.brapi.test.BrAPITestServer.model.entity.BrAPIBaseEntity; import org.brapi.test.BrAPITestServer.model.entity.ExternalReferenceEntity; import org.brapi.test.BrAPITestServer.model.entity.core.CropEntity; +import org.brapi.test.BrAPITestServer.model.entity.core.ProgramEntity; import org.brapi.test.BrAPITestServer.model.entity.germ.*; import org.brapi.test.BrAPITestServer.model.entity.germ.GermplasmInstituteEntity.InstituteTypeEnum; import org.brapi.test.BrAPITestServer.model.entity.pheno.TaxonEntity; @@ -23,6 +25,7 @@ import org.brapi.test.BrAPITestServer.service.SearchQueryBuilder; import org.brapi.test.BrAPITestServer.service.UpdateUtility; import org.brapi.test.BrAPITestServer.service.core.CropService; +import org.brapi.test.BrAPITestServer.service.core.ProgramService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -44,21 +47,23 @@ public class GermplasmService { private final GermplasmDonorRepository donorRepository; private final BreedingMethodService breedingMethodService; private final CropService cropService; + private final ProgramService programService; @Autowired public GermplasmService(GermplasmRepository germplasmRepository, GermplasmDonorRepository donorRepository, - BreedingMethodService breedingMethodService, CropService cropService) { + BreedingMethodService breedingMethodService, CropService cropService, ProgramService programService) { this.germplasmRepository = germplasmRepository; this.donorRepository = donorRepository; this.breedingMethodService = breedingMethodService; this.cropService = cropService; + this.programService = programService; } public List findGermplasm(String germplasmPUI, String germplasmDbId, String germplasmName, String accessionNumber, String collection, String binomialName, String genus, String species, String trialDbId, String studyDbId, String synonym, String parentDbId, String progenyDbId, - String commonCropName, String programDbId, String externalReferenceId, String externalReferenceID, + String commonCropName, String programDbId, String programName, String externalReferenceId, String externalReferenceID, String externalReferenceSource, Metadata metadata) throws BrAPIServerException { @@ -93,6 +98,8 @@ public List findGermplasm(String germplasmPUI, String germplasmDbId, request.addCommonCropNamesItem(commonCropName); if (programDbId != null) request.addProgramDbIdsItem(programDbId); + if (programName != null) + request.addProgramNamesItem(programName); request.addExternalReferenceItem(externalReferenceId, externalReferenceID, externalReferenceSource); @@ -171,12 +178,10 @@ private SearchQueryBuilder buildGermplasmSearchQuery(GermplasmS .leftJoinFetch("pedigree", "pedigree") .leftJoinFetch("*pedigree.crossingProject", "crossingProject"); - if (request.getProgramDbIds() != null || request.getProgramNames() != null || request.getTrialDbIds() != null + if (request.getTrialDbIds() != null || request.getTrialNames() != null || request.getStudyDbIds() != null || request.getStudyNames() != null) { searchQuery = searchQuery.join("observationUnits", "obsunit") - .appendList(request.getProgramDbIds(), "*obsunit.program.id") - .appendList(request.getProgramNames(), "*obsunit.program.name") .appendList(request.getTrialDbIds(), "*obsunit.trial.id") .appendList(request.getTrialNames(), "*obsunit.trial.name") .appendList(request.getStudyDbIds(), "*obsunit.study.id") @@ -197,6 +202,7 @@ private SearchQueryBuilder buildGermplasmSearchQuery(GermplasmS .appendList(request.getGermplasmNames(), "germplasmName") .appendList(request.getGermplasmPUIs(), "germplasmPUI") .appendList(request.getParentDbIds(), "pedigree.parent1.germplasm.id") + .appendList(request.getProgramDbIds(), "program.id").appendList(request.getProgramNames(), "program.name") // .appendList(request.getProgenyDbIds(), "*progeny.germplasmDbId") .appendList(request.getGenus(), "genus").appendList(request.getSpecies(), "species") .appendNamesList(request.getBinomialNames(), "genus", "genus", "species") @@ -528,6 +534,12 @@ private Germplasm convertFromEntity(GermplasmEntity entity) { germ.setCollection(entity.getCollection()); if (entity.getCrop() != null) germ.setCommonCropName(entity.getCrop().getCropName()); + + if (entity.getProgram() != null) { + germ.setProgramDbId(entity.getProgram().getId().toString()); + germ.setProgramName(entity.getProgram().getName()); + } + germ.setCountryOfOriginCode(entity.getCountryOfOriginCode()); germ.setDefaultDisplayName(entity.getDefaultDisplayName()); germ.setDocumentationURL(entity.getDocumentationURL()); @@ -581,6 +593,11 @@ private List createEntitiesInBatch(List bo .filter(Objects::nonNull) .collect(Collectors.toSet()); + Set programDbIds = body.stream() + .map(GermplasmNewRequest::getProgramDbId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + Map foundBreedingMethodsById = breedingMethodService.findBreedingMethodsByIds(breedingMethodIds) .stream() @@ -590,6 +607,11 @@ private List createEntitiesInBatch(List bo .stream() .collect(Collectors.toMap(CropEntity::getCropName, e -> e)); + Map foundProgramsByDbId + = programService.findByIds(new ArrayList<>(programDbIds)) + .stream() + .collect(Collectors.toMap(ProgramEntity::getId, e -> e)); + for (GermplasmNewRequest request : body) { GermplasmEntity entity = new GermplasmEntity(); @@ -604,7 +626,7 @@ private List createEntitiesInBatch(List bo if (request.getBiologicalStatusOfAccessionCode() != null) entity.setBiologicalStatusOfAccessionCode(request.getBiologicalStatusOfAccessionCode()); if (request.getBreedingMethodDbId() != null) { - entity.setBreedingMethod(foundBreedingMethodsById.get(request.getBreedingMethodDbId())); + entity.setBreedingMethod(foundBreedingMethodsById.get(UUID.fromString(request.getBreedingMethodDbId()))); } if (request.getCollection() != null) entity.setCollection(request.getCollection()); @@ -661,6 +683,10 @@ private List createEntitiesInBatch(List bo updateSynonymEntities(request.getSynonyms(), entity); if (request.getTaxonIds() != null) updateTaxonEntities(request.getTaxonIds(), entity); + if (!foundProgramsByDbId.isEmpty()) { + ProgramEntity program = foundProgramsByDbId.get(UUID.fromString(request.getProgramDbId())); + entity.setProgram(program); + } toSave.add(entity); } @@ -738,6 +764,10 @@ private void updateEntity(GermplasmEntity entity, GermplasmNewRequest request) t updateSynonymEntities(request.getSynonyms(), entity); if (request.getTaxonIds() != null) updateTaxonEntities(request.getTaxonIds(), entity); + if (request.getProgramDbId() != null) { + ProgramEntity program = programService.getProgramEntity(request.getProgramDbId()); + entity.setProgram(program); + } } private void updateTaxonEntities(List taxonIds, GermplasmEntity entity) { From 5a747b473dcc651ace2acc823f88f620b989b168 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Tue, 18 Aug 2026 16:20:13 -0400 Subject: [PATCH 20/26] Use more restrictive exref query, add assertion to migration --- .../V006_001__migrate_parent_ou_ids.sql | 53 ++++++++++++++++--- 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/src/main/resources/db/migration/V006_001__migrate_parent_ou_ids.sql b/src/main/resources/db/migration/V006_001__migrate_parent_ou_ids.sql index 3dd227b5..1cb2c600 100644 --- a/src/main/resources/db/migration/V006_001__migrate_parent_ou_ids.sql +++ b/src/main/resources/db/migration/V006_001__migrate_parent_ou_ids.sql @@ -9,15 +9,26 @@ WITH FROM observation_unit_level WHERE level_code LIKE '% %' ), + -- This query should pick up external references with a unique external_reference_id and external_reference_source. + -- This is critical for this update as if there are any external references used that are not unique, + -- we could assign the wrong observation_unit_id. + exrefid_source_with_one_ou_connected AS ( + SELECT ex.external_reference_id, ex.external_reference_source, (array_agg(id))[1] AS exref_pk +FROM external_reference ex + JOIN observation_unit_external_references ouex ON ex.id = ouex.external_references_id +WHERE ex.external_reference_source = 'breedinginsight.org/observationunits' +GROUP BY ex.external_reference_id, ex.external_reference_source +HAVING count(*) = 1 + ), ou_ids_matched_on_levels AS ( -- Now match the bi-generated exref ou ids to ex refs ids, and keep observation_unit_level ids for matching in next part - SELECT - ou.id AS ou_id, - levels_and_parent_ou_bi_id.level_id - FROM observation_unit ou - JOIN observation_unit_external_references ouex ON ou.id = ouex.observation_unit_entity_id - JOIN external_reference ex ON ouex.external_references_id = ex.id - JOIN levels_and_parent_ou_bi_id ON ex.external_reference_id = levels_and_parent_ou_bi_id.parent_bi_ou_id +SELECT + ou.id AS ou_id, + levels_and_parent_ou_bi_id.level_id +FROM observation_unit ou + JOIN observation_unit_external_references ouex ON ou.id = ouex.observation_unit_entity_id + JOIN exrefid_source_with_one_ou_connected ON exrefid_source_with_one_ou_connected.exref_pk = ouex.external_references_id + JOIN levels_and_parent_ou_bi_id ON exrefid_source_with_one_ou_connected.external_reference_id = levels_and_parent_ou_bi_id.parent_bi_ou_id ) UPDATE observation_unit_level SET level_code = regexp_replace( @@ -26,4 +37,30 @@ SET level_code = regexp_replace( mol.ou_id::text ) FROM ou_ids_matched_on_levels mol -WHERE observation_unit_level.id = mol.level_id \ No newline at end of file +WHERE observation_unit_level.id = mol.level_id + +-- Assertion to assure we don't remove additional_info.observationLevel where +-- references can't be updated. For our prod data, shouldn't be any. +DO $$ +DECLARE + top_level_observation_unit_level_count integer; + top_level_ouls_matched_to_ou_id integer; +BEGIN + SELECT COUNT(*) + INTO top_level_observation_unit_level_count + FROM observation_unit_level + WHERE level_code like '% %'; + + SELECT COUNT(*) + INTO top_level_ouls_matched_to_ou_id + FROM observation_unit_level oul + JOIN observation_unit ou on ou.id::text = substring(level_code FROM '^([^ ]+)') + WHERE level_code like '% %'; + + IF top_level_observation_unit_level_count <> top_level_ouls_matched_to_ou_id THEN + RAISE EXCEPTION + 'V006.001 After migration, expected all % observation_unit_level.code rows to match to observation_unit.id, but only % matched', + top_level_observation_unit_level_count, + top_level_ouls_matched_to_ou_id; + END IF; +END $$; \ No newline at end of file From c17b97a3fe74b95475522d07097f347869472342 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Tue, 18 Aug 2026 16:42:05 -0400 Subject: [PATCH 21/26] Add semicolon --- .../resources/db/migration/V006_001__migrate_parent_ou_ids.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/db/migration/V006_001__migrate_parent_ou_ids.sql b/src/main/resources/db/migration/V006_001__migrate_parent_ou_ids.sql index 1cb2c600..a6413c46 100644 --- a/src/main/resources/db/migration/V006_001__migrate_parent_ou_ids.sql +++ b/src/main/resources/db/migration/V006_001__migrate_parent_ou_ids.sql @@ -37,7 +37,7 @@ SET level_code = regexp_replace( mol.ou_id::text ) FROM ou_ids_matched_on_levels mol -WHERE observation_unit_level.id = mol.level_id +WHERE observation_unit_level.id = mol.level_id; -- Assertion to assure we don't remove additional_info.observationLevel where -- references can't be updated. For our prod data, shouldn't be any. From cc05724e809243ad6eb1d187149b2d9f8ed02775 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Tue, 18 Aug 2026 20:25:51 -0400 Subject: [PATCH 22/26] Update comment --- .../resources/db/migration/V006_001__migrate_parent_ou_ids.sql | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/resources/db/migration/V006_001__migrate_parent_ou_ids.sql b/src/main/resources/db/migration/V006_001__migrate_parent_ou_ids.sql index a6413c46..2166b229 100644 --- a/src/main/resources/db/migration/V006_001__migrate_parent_ou_ids.sql +++ b/src/main/resources/db/migration/V006_001__migrate_parent_ou_ids.sql @@ -39,8 +39,7 @@ SET level_code = regexp_replace( FROM ou_ids_matched_on_levels mol WHERE observation_unit_level.id = mol.level_id; --- Assertion to assure we don't remove additional_info.observationLevel where --- references can't be updated. For our prod data, shouldn't be any. +-- Assertion to assure all expected top-level observation unit db ids were changed from external reference ids to observation unit db ids DO $$ DECLARE top_level_observation_unit_level_count integer; From cc82ecdc4458c1dfbd1238f1bc02e82ebc50711b Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Tue, 18 Aug 2026 20:46:48 -0400 Subject: [PATCH 23/26] Update migration script with stricter matching to OU, add assertion --- .../V006_002__migrate_sample_ou_ids.sql | 58 +++++++++++++++---- 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/src/main/resources/db/migration/V006_002__migrate_sample_ou_ids.sql b/src/main/resources/db/migration/V006_002__migrate_sample_ou_ids.sql index e944c439..5ff206d3 100644 --- a/src/main/resources/db/migration/V006_002__migrate_sample_ou_ids.sql +++ b/src/main/resources/db/migration/V006_002__migrate_sample_ou_ids.sql @@ -4,24 +4,58 @@ WITH SELECT id as sample_id, additional_info ->> 'obsUnitID' AS bi_ou_id -FROM sample -WHERE additional_info ? 'obsUnitID' + FROM sample + WHERE additional_info ? 'obsUnitID' + ), + exrefid_source_with_one_ou_connected AS ( + SELECT ex.external_reference_id, ex.external_reference_source, (array_agg(id))[1] AS exref_pk + FROM external_reference ex + JOIN observation_unit_external_references ouex ON ex.id = ouex.external_references_id + WHERE ex.external_reference_source = 'breedinginsight.org/observationunits' + GROUP BY ex.external_reference_id, ex.external_reference_source + HAVING COUNT(*) = 1 ), ou_ids_matched_on_samples AS ( -- Now match the bi-generated exref ou ids to ex refs ids, and keep samples ids for matching in update -SELECT - ou.id AS ou_id, - samples_and_bi_ou_ids.sample_id -FROM observation_unit ou - JOIN observation_unit_external_references ouex ON ou.id = ouex.observation_unit_entity_id - JOIN external_reference ex ON ouex.external_references_id = ex.id - JOIN samples_and_bi_ou_ids ON ex.external_reference_id = samples_and_bi_ou_ids.bi_ou_id + SELECT + ou.id AS ou_id, + samples_and_bi_ou_ids.sample_id + FROM observation_unit ou + JOIN observation_unit_external_references ouex ON ou.id = ouex.observation_unit_entity_id + JOIN exrefid_source_with_one_ou_connected on exrefid_source_with_one_ou_connected.exref_pk = ouex.external_references_id + JOIN samples_and_bi_ou_ids ON exrefid_source_with_one_ou_connected.external_reference_id = samples_and_bi_ou_ids.bi_ou_id ) UPDATE sample SET additional_info = jsonb_set( additional_info, '{obsUnitID}', to_jsonb(mos.ou_id) - ) - FROM ou_ids_matched_on_samples mos -WHERE id = mos.sample_id \ No newline at end of file +) +FROM ou_ids_matched_on_samples mos +WHERE id = mos.sample_id; + +-- Assertion to ensure all samples with ou in additional info now relate to observation unit db id +DO $$ + DECLARE + samples_with_ou_count integer; + samples_with_ous_matched_to_ou_count integer; + BEGIN + SELECT COUNT(*) + INTO samples_with_ou_count + FROM sample + WHERE additional_info ? 'obsUnitID'; + + SELECT COUNT(*) + INTO samples_with_ous_matched_to_ou_count + FROM sample s + JOIN observation_unit ou on ou.id::text = s.additional_info ->> 'obsUnitID' + WHERE s.additional_info ? 'obsUnitID'; + + IF samples_with_ou_count <> samples_with_ous_matched_to_ou_count THEN + RAISE EXCEPTION + 'V006.002 After migration, expected all % sample.additional_info->>obsUnitID rows to match to observation_unit.id, but only % matched', + samples_with_ou_count, + samples_with_ous_matched_to_ou_count; + END IF; + END +$$; \ No newline at end of file From b30833000468b5ef8d0b31bae7cb43a9d0db7e41 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Thu, 20 Aug 2026 16:31:19 -0400 Subject: [PATCH 24/26] Undo removed changes from develop --- pom.xml | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a1e201c5..e5017b16 100644 --- a/pom.xml +++ b/pom.xml @@ -152,7 +152,7 @@ org.springframework.boot spring-boot-maven-plugin - ${spring.version} + 2.7.5 @@ -164,4 +164,21 @@ + + + spring-releases + https://repo.spring.io/libs-release + + + spring-milestone + https://repo.spring.io/libs-milestone + + + + + spring-releases + https://repo.spring.io/libs-release + + + From 265da1423c9e074c6ac6c003ec852274e8a7ede6 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Mon, 24 Aug 2026 12:38:44 -0400 Subject: [PATCH 25/26] Ignore getMethod on sortFilter map that was breaking async use case --- src/main/java/io/swagger/model/SearchRequest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/io/swagger/model/SearchRequest.java b/src/main/java/io/swagger/model/SearchRequest.java index aaad3b9a..b59090fe 100644 --- a/src/main/java/io/swagger/model/SearchRequest.java +++ b/src/main/java/io/swagger/model/SearchRequest.java @@ -207,6 +207,7 @@ public void setSortBy(List sortBy) throws BrAPIServerException { this.sortBy = sortBy; } + @JsonIgnore public Map getEntityColAndTypeBySubmittedNameMap() throws BrAPIServerException { throw new BrAPIServerException(HttpStatus.BAD_REQUEST, String.format("Sort/Filtering not implemented for %s", this.getClass().getSimpleName())); } From 5a39677a17cabe8ee247fd4649369efd362ec069 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Wed, 26 Aug 2026 13:39:08 -0400 Subject: [PATCH 26/26] Fix createEntities in batch error, address migration concerns --- .../service/germ/GermplasmService.java | 2 +- ...dd_program_id_to_germplasm_and_migrate.sql | 28 +++++++++++++++---- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java index 2d36e506..5d3e4689 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java @@ -684,7 +684,7 @@ private List createEntitiesInBatch(List bo updateSynonymEntities(request.getSynonyms(), entity); if (request.getTaxonIds() != null) updateTaxonEntities(request.getTaxonIds(), entity); - if (!foundProgramsByDbId.isEmpty()) { + if (request.getProgramDbId() != null) { ProgramEntity program = foundProgramsByDbId.get(UUID.fromString(request.getProgramDbId())); entity.setProgram(program); } diff --git a/src/main/resources/db/migration/V006_003__add_program_id_to_germplasm_and_migrate.sql b/src/main/resources/db/migration/V006_003__add_program_id_to_germplasm_and_migrate.sql index 9f5b2aae..eb13b87a 100644 --- a/src/main/resources/db/migration/V006_003__add_program_id_to_germplasm_and_migrate.sql +++ b/src/main/resources/db/migration/V006_003__add_program_id_to_germplasm_and_migrate.sql @@ -4,19 +4,37 @@ ADD COLUMN program_id UUID; ALTER TABLE germplasm ADD CONSTRAINT germplasm_program_fk FOREIGN KEY (program_id) - REFERENCES public.program(id) - ON DELETE CASCADE; + REFERENCES public.program(id); CREATE INDEX germplasm_program_idx ON germplasm (program_id, id); UPDATE germplasm set program_id = pquery.program_id FROM ( SELECT g.id AS germplasm_id, p.id AS program_id - from germplasm g + FROM germplasm g JOIN germplasm_external_references gex ON g.id = gex.germplasm_entity_id JOIN external_reference ex ON ex.id = gex.external_references_id JOIN external_reference ex2 ON ex2.external_reference_id = ex.external_reference_id JOIN program_external_references pex ON pex.external_references_id = ex2.id JOIN program p ON p.id = pex.program_entity_id -) pquery -where id = pquery.germplasm_id; \ No newline at end of file + WHERE ex.external_reference_source = 'breedinginsight.org/programs' AND ex2.external_reference_source = 'breedinginsight.org') pquery +WHERE id = pquery.germplasm_id; + +-- Assertion to ensure all germplasm are now associated with a program +DO $$ + DECLARE + germs_without_program_count integer; + + BEGIN + SELECT COUNT(*) + INTO germs_without_program_count + FROM germplasm g + WHERE g.program_id IS NULL; + + IF germs_without_program_count <> 0 THEN + RAISE EXCEPTION + 'V006.003 After migration, expected all germplasm rows to have program_id set, but there are % rows with program_id not set', + germs_without_program_count; + END IF; + END +$$; \ No newline at end of file