From f2f8cf5668b2ffc3c2df2f564e2641a7a7f093c6 Mon Sep 17 00:00:00 2001 From: Xing Yang <5168106+XingY@users.noreply.github.com> Date: Tue, 19 May 2026 13:17:27 -0700 Subject: [PATCH 1/5] Remove aliquot data warning logging (#7672) --- .../api/dataiterator/SimpleTranslator.java | 30 ++++--------------- .../api/SampleTypeUpdateServiceDI.java | 11 ++----- 2 files changed, 8 insertions(+), 33 deletions(-) diff --git a/api/src/org/labkey/api/dataiterator/SimpleTranslator.java b/api/src/org/labkey/api/dataiterator/SimpleTranslator.java index 8480f6f93bd..3016883bdb1 100644 --- a/api/src/org/labkey/api/dataiterator/SimpleTranslator.java +++ b/api/src/org/labkey/api/dataiterator/SimpleTranslator.java @@ -57,7 +57,6 @@ import org.labkey.api.data.TableSelector; import org.labkey.api.data.TestSchema; import org.labkey.api.exp.MvFieldWrapper; -import org.labkey.api.exp.PropertyDescriptor; import org.labkey.api.exp.PropertyType; import org.labkey.api.files.FileContentService; import org.labkey.api.ontology.Unit; @@ -486,20 +485,16 @@ protected class DerivationScopedConvertColumn extends SimpleConvertColumn final int derivationDataColInd; final int index; final boolean isDerivation; - final String presentDerivationWarning; - final String presentNonDerivationWarning; final SimpleConvertColumn _convertCol; - public DerivationScopedConvertColumn(int index, SimpleConvertColumn convertCol, int derivationDataColInd, boolean isDerivation, @Nullable String presentDerivationWarning, @Nullable String presentNonDerivationWarning) + public DerivationScopedConvertColumn(int index, SimpleConvertColumn convertCol, int derivationDataColInd, boolean isDerivation) { super(convertCol.fieldName, convertCol.index, convertCol.type, convertCol.defaultUnit); _convertCol = convertCol; this.index = index; this.derivationDataColInd = derivationDataColInd; this.isDerivation = isDerivation; - this.presentDerivationWarning = presentDerivationWarning; - this.presentNonDerivationWarning = presentNonDerivationWarning; } @Override @@ -507,7 +502,7 @@ protected Object convert(Object o) { Object thisValue = _convertCol.convert(o); - return getDerivationData(thisValue, derivationDataColInd, isDerivation, presentDerivationWarning, presentNonDerivationWarning); + return getDerivationData(thisValue, derivationDataColInd, isDerivation); } } @@ -520,23 +515,18 @@ protected class DerivationScopedColumn implements Supplier final int index; final boolean isDerivation; - final String presentDerivationWarning; - final String presentNonDerivationWarning; - - public DerivationScopedColumn(int index, int derivationDataColInd, boolean isDerivation, @Nullable String presentDerivationWarning, @Nullable String presentNonDerivationWarning) + public DerivationScopedColumn(int index, int derivationDataColInd, boolean isDerivation) { this.index = index; this.derivationDataColInd = derivationDataColInd; this.isDerivation = isDerivation; - this.presentDerivationWarning = presentDerivationWarning; - this.presentNonDerivationWarning = presentNonDerivationWarning; } @Override public Object get() { Object thisValue = _data.get(index); - return getDerivationData(thisValue, derivationDataColInd, isDerivation, presentDerivationWarning, presentNonDerivationWarning); + return getDerivationData(thisValue, derivationDataColInd, isDerivation); } } @@ -544,24 +534,14 @@ public Object get() * @param thisValue the original field value * @param derivationDataColInd the col index for the field used to determine if a record is child or parent * @param isDerivationField if this field is a child only field - * @param presentDerivationWarning the warning msg to log if a child field is present for a parent record - * @param presentNonDerivationWarning the warning msg to log if a parent field is present for a child record */ - private Object getDerivationData(Object thisValue, int derivationDataColInd, boolean isDerivationField, @Nullable String presentDerivationWarning, @Nullable String presentNonDerivationWarning) + private Object getDerivationData(Object thisValue, int derivationDataColInd, boolean isDerivationField) { Object derivationData = derivationDataColInd < 0 ? null : _data.get(derivationDataColInd); if ((isDerivationField && derivationData != null) || (!isDerivationField && derivationData == null)) return thisValue; - if (thisValue != null) - { - if (isDerivationField && presentDerivationWarning != null) - LOG.warn(presentDerivationWarning); - else if (!isDerivationField && presentNonDerivationWarning != null) - LOG.warn(presentNonDerivationWarning); - } - return null; } diff --git a/experiment/src/org/labkey/experiment/api/SampleTypeUpdateServiceDI.java b/experiment/src/org/labkey/experiment/api/SampleTypeUpdateServiceDI.java index bd8273f8d99..571c47a609a 100644 --- a/experiment/src/org/labkey/experiment/api/SampleTypeUpdateServiceDI.java +++ b/experiment/src/org/labkey/experiment/api/SampleTypeUpdateServiceDI.java @@ -1753,9 +1753,6 @@ private boolean rowExists(String name) static class _SamplesCoerceDataIterator extends SimpleTranslator { - private static final String INVALID_ALIQUOT_PROPERTY = "An aliquot-specific property [%1$s] value has been ignored for a non-aliquot sample."; - private static final String INVALID_NON_ALIQUOT_PROPERTY = "A sample property [%1$s] value has been ignored for an aliquot."; - private final ExpSampleTypeImpl _sampleType; private final Unit _sampleTypeBaseUnit; @@ -1810,14 +1807,12 @@ else if (amountImportAliasSet.contains(from.getName())) String name = to.getName(); boolean isScopedField = scopedFields.containsKey(name); - String ignoredAliquotPropValue = String.format(INVALID_ALIQUOT_PROPERTY, name); - String ignoredMetaPropValue = String.format(INVALID_NON_ALIQUOT_PROPERTY, name); if (to.getPropertyType() == PropertyType.ATTACHMENT || to.getPropertyType() == PropertyType.FILE_LINK) { if (isScopedField) { ColumnInfo clone = new BaseColumnInfo(to); - addColumn(clone, new DerivationScopedColumn(i, aliquotedFromDataColInd, scopedFields.get(name), ignoredAliquotPropValue, ignoredMetaPropValue)); + addColumn(clone, new DerivationScopedColumn(i, aliquotedFromDataColInd, scopedFields.get(name))); } else addColumn(to, i); @@ -1829,7 +1824,7 @@ else if (to.isMultiValued() || to.getFk() instanceof MultiValuedForeignKey) { var col = new BaseColumnInfo(getInput().getColumnInfo(i)); col.setName(name); - addColumn(col, new DerivationScopedColumn(i, aliquotedFromDataColInd, scopedFields.get(name), ignoredAliquotPropValue, ignoredMetaPropValue)); + addColumn(col, new DerivationScopedColumn(i, aliquotedFromDataColInd, scopedFields.get(name))); } else addColumn(to.getName(), i); @@ -1900,7 +1895,7 @@ private void _addConvertColumn(String name, int fromIndex, JdbcType toType, @Nul private void _addConvertColumn(ColumnInfo col, int fromIndex, int derivationDataColInd, boolean isAliquotField) { SimpleConvertColumn c = createConvertColumn(col, fromIndex, RemapMissingBehavior.Error); - c = new DerivationScopedConvertColumn(fromIndex, c, derivationDataColInd, isAliquotField, String.format(INVALID_ALIQUOT_PROPERTY, col.getName()), String.format(INVALID_NON_ALIQUOT_PROPERTY, col.getName())); + c = new DerivationScopedConvertColumn(fromIndex, c, derivationDataColInd, isAliquotField); addColumn(col, c); } From 96a3439caaff0255a4635a8bad3b623c19c7ca1b Mon Sep 17 00:00:00 2001 From: Josh Eckels Date: Wed, 20 May 2026 15:20:31 -0700 Subject: [PATCH 2/5] Prevent popup widget from wrapping to next line (#7673) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### Rationale Protein name display looks better when the popup widget isn't wrapped to the next line in a table cell. #### Changes - Inject a non-breaking space to ensure no inopportune wrapping #### Tasks 📍 - [x] Claude Code Review - [x] Manual Testing @labkey-tchad - Create a Panorama folder (subtype: Experiment) - Import MRMer.zip (https://github.com/LabKey/targetedms/blob/develop/test/sampledata/TargetedMS/MRMer.zip) - Click the Peptide Groups link in the runs grid - Check that the protein name and down-arrow render without wrapping - [x] Verify Safari - [x] Verify Firefox - [x] Verify Chrome - ~Test Automation~ image --- api/src/org/labkey/api/action/QueryViewAction.java | 4 ++++ api/src/org/labkey/api/data/AJAXDetailsDisplayColumn.java | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/api/src/org/labkey/api/action/QueryViewAction.java b/api/src/org/labkey/api/action/QueryViewAction.java index 9ebb9540eac..42a02c7bae6 100644 --- a/api/src/org/labkey/api/action/QueryViewAction.java +++ b/api/src/org/labkey/api/action/QueryViewAction.java @@ -16,6 +16,7 @@ package org.labkey.api.action; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.labkey.api.data.ColumnHeaderType; import org.labkey.api.data.ExcelWriter; @@ -135,7 +136,9 @@ protected ModelAndView getHtmlView(Form form, BindException errors) throws Excep /** * Correctly configures the QueryView to use the QueryViewAction for export purposes * @param dataRegion null as a convenience when only a single QueryView is being used + * @throws NotFoundException if passed a dataRegion name that doesn't exist */ + @NotNull protected final ViewType createInitializedQueryView(Form form, BindException errors, boolean forExport, @Nullable String dataRegion) throws Exception { ViewType result = createQueryView(form, errors, forExport, dataRegion); @@ -164,6 +167,7 @@ protected final ViewType createInitializedQueryView(Form form, BindException err * Create the specially configured query view. * @param dataRegion null as a convenience when only a single QueryView is being used */ + @Nullable protected abstract ViewType createQueryView(Form form, BindException errors, boolean forExport, @Nullable String dataRegion) throws Exception; public static class QueryExportForm extends QueryForm diff --git a/api/src/org/labkey/api/data/AJAXDetailsDisplayColumn.java b/api/src/org/labkey/api/data/AJAXDetailsDisplayColumn.java index c750658316f..f6e52614f42 100644 --- a/api/src/org/labkey/api/data/AJAXDetailsDisplayColumn.java +++ b/api/src/org/labkey/api/data/AJAXDetailsDisplayColumn.java @@ -23,6 +23,7 @@ import org.labkey.api.util.ContainerContext; import org.labkey.api.util.DOM; import org.labkey.api.util.GUID; +import org.labkey.api.util.HtmlString; import org.labkey.api.util.JavaScriptFragment; import org.labkey.api.util.StringExpression; import org.labkey.api.view.ActionURL; @@ -38,6 +39,7 @@ import static org.labkey.api.util.DOM.SPAN; import static org.labkey.api.util.DOM.id; +import static org.labkey.api.util.DOM.Attribute.style; /** * Uses LABKEY.Ext.CalloutTip to provide additional details, summoned via AJAX @@ -106,7 +108,7 @@ public void renderGridCellContents(RenderContext ctx, HtmlWriter out) props.put("target", divId); SPAN( - id(divId), + id(divId).at(style, "display:inline-flex;flex-wrap:nowrap"), (DOM.Renderable) ret -> { super.renderGridCellContents(ctx, out); return ret; From bc43b81b0eec788d8620739aa59f04245ffa20c0 Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Thu, 21 May 2026 08:15:49 -0700 Subject: [PATCH 3/5] Audit schema migration: register handler and cast HasDetails varchar->boolean (#7669) - `AuditModule.registerMigrationHandlers` registers the audit schema with `DatabaseMigrationService` so all source rows are carried. - `HasDetailsCastColumn` (private static `WrappedColumn`) wraps the `HasDetails` column to emit `CAST(... AS BIT)` in the source SELECT and report `JdbcType.BOOLEAN`. Co-authored-by: Claude Opus 4.7 (1M context) --- audit/src/org/labkey/audit/AuditModule.java | 61 +++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/audit/src/org/labkey/audit/AuditModule.java b/audit/src/org/labkey/audit/AuditModule.java index 5be6326ee39..ac6788c32d2 100644 --- a/audit/src/org/labkey/audit/AuditModule.java +++ b/audit/src/org/labkey/audit/AuditModule.java @@ -19,6 +19,13 @@ import org.jetbrains.annotations.NotNull; import org.labkey.api.audit.AuditLogService; import org.labkey.api.audit.provider.SiteSettingsAuditProvider; +import org.labkey.api.data.ColumnInfo; +import org.labkey.api.data.JdbcType; +import org.labkey.api.data.SQLFragment; +import org.labkey.api.data.TableInfo; +import org.labkey.api.data.WrappedColumn; +import org.labkey.api.migration.DatabaseMigrationService; +import org.labkey.api.migration.MigrationTableHandler; import org.labkey.api.module.DefaultModule; import org.labkey.api.module.ModuleContext; import org.labkey.api.view.WebPartFactory; @@ -27,6 +34,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Set; +import java.util.TreeSet; public class AuditModule extends DefaultModule { @@ -89,4 +97,57 @@ public Set getProvisionedSchemaNames() { return Collections.singleton(AuditSchema.SCHEMA_NAME); } + + private final Set _registeredMigrationTables = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + + @Override + public void registerMigrationHandlers(@NotNull DatabaseMigrationService service) + { + service.registerSchemaContributor(AuditSchema.SCHEMA_NAME, schema -> + schema.getTableNames().stream() + .map(schema::getTable) + .filter(table -> table != null && _registeredMigrationTables.add(table.getSelectName())) + .forEach(table -> service.registerTableHandler(new MigrationTableHandler() + { + @Override + public TableInfo getTableInfo() + { + return table; + } + + @Override + public ColumnInfo handleColumn(ColumnInfo col) + { + if ("hasdetails".equalsIgnoreCase(col.getName())) + return new HasDetailsCastColumn(col); + return col; + } + }))); + } + + // Some legacy source databases store the DatasetAuditDomain "HasDetails" column as varchar + // while the target was provisioned as boolean. CAST to BIT in the source SELECT and report + // BOOLEAN as the JdbcType so the migration's INSERT parameter binds cleanly into the + // target's boolean column (otherwise the parameter inherits the source column's VARCHAR + // type and PG rejects the bound value). + private static final class HasDetailsCastColumn extends WrappedColumn + { + private HasDetailsCastColumn(ColumnInfo col) + { + super(col, col.getName()); + } + + @Override + @NotNull + public JdbcType getJdbcType() + { + return JdbcType.BOOLEAN; + } + + @Override + public SQLFragment getValueSql(String tableAlias) + { + return new SQLFragment("CAST(").append(super.getValueSql(tableAlias)).append(" AS BIT)"); + } + } } From 6b8e89c4a5ab920947c08227abd0bce1da0bb7c6 Mon Sep 17 00:00:00 2001 From: Josh Eckels Date: Fri, 22 May 2026 11:25:33 -0700 Subject: [PATCH 4/5] Partial backport of platform PR #7435 (#7685) #### Rationale Simplify the `study` schema's queries. #### Changes - Remove unused table --- .../IVisualizationSourceQuery.java | 2 - .../labkey/study/query/StudyQuerySchema.java | 22 ---- .../query/VisualizationVisitTagTable.java | 104 ------------------ .../StudyVisualizationProvider.java | 41 ++++--- .../sql/OuterJoinSourceQuery.java | 6 - .../sql/VisualizationSourceQuery.java | 17 +-- 6 files changed, 24 insertions(+), 168 deletions(-) delete mode 100644 study/src/org/labkey/study/query/VisualizationVisitTagTable.java diff --git a/api/src/org/labkey/api/visualization/IVisualizationSourceQuery.java b/api/src/org/labkey/api/visualization/IVisualizationSourceQuery.java index 0fc293bb331..9c017057af5 100644 --- a/api/src/org/labkey/api/visualization/IVisualizationSourceQuery.java +++ b/api/src/org/labkey/api/visualization/IVisualizationSourceQuery.java @@ -63,8 +63,6 @@ public interface IVisualizationSourceQuery boolean isSkipVisitJoin(); - boolean isVisitTagQuery(); - /** * True if any select or aggregate requires a left join explicitly. This is an override for any columns * that might require some form of an INNER JOIN. diff --git a/study/src/org/labkey/study/query/StudyQuerySchema.java b/study/src/org/labkey/study/query/StudyQuerySchema.java index 3a1ca6143e4..9c31108a5e3 100644 --- a/study/src/org/labkey/study/query/StudyQuerySchema.java +++ b/study/src/org/labkey/study/query/StudyQuerySchema.java @@ -124,7 +124,6 @@ public class StudyQuerySchema extends UserSchema implements UserSchema.HasContex public static final String VISIT_TAG_TABLE_NAME = "VisitTag"; public static final String VISIT_TAG_MAP_TABLE_NAME = "VisitTagMap"; public static final String VISIT_ALIASES = "VisitAliases"; - public static final String VISUALIZATION_VISIT_TAG_TABLE_NAME = "VisualizationVisitTag"; public static final String VISIT_MAP_TABLE_NAME = "VisitMap"; public static final String STUDY_DATA_TABLE_NAME = "StudyData"; @@ -642,27 +641,6 @@ public TableInfo createTable(String name, ContainerFilter cf) { return new VisitMapTable(this, cf); } - if (name.startsWith(VISUALIZATION_VISIT_TAG_TABLE_NAME)) - { - // Name is encoded with useProtocolDay boolean, tagName, and altQueryName - String params = name.substring(VISUALIZATION_VISIT_TAG_TABLE_NAME.length()); - boolean useProtocolDay; - if (params.startsWith("-true")) - { - params = params.substring(params.indexOf("-true") + 6); - useProtocolDay = true; - } - else - { - params = params.substring(params.indexOf("-false") + 7); - useProtocolDay = false; - } - int hyphenIndex = params.indexOf("-"); - String tagName = hyphenIndex > -1 ? params.substring(0, hyphenIndex) : params; - String altQueryName = hyphenIndex > -1 ? params.substring(hyphenIndex + 1) : null; - - return new VisualizationVisitTagTable(this, cf, getStudy(), getUser(), tagName, useProtocolDay, altQueryName); - } // Might be a dataset DatasetDefinition dsd = getDatasetDefinitionByQueryName(name); diff --git a/study/src/org/labkey/study/query/VisualizationVisitTagTable.java b/study/src/org/labkey/study/query/VisualizationVisitTagTable.java deleted file mode 100644 index f1c277ec7e8..00000000000 --- a/study/src/org/labkey/study/query/VisualizationVisitTagTable.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (c) 2014-2019 LabKey Corporation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.labkey.study.query; - -import org.jetbrains.annotations.NotNull; -import org.labkey.api.data.ContainerFilter; -import org.labkey.api.data.JdbcType; -import org.labkey.api.data.SQLFragment; -import org.labkey.api.data.VirtualTable; -import org.labkey.api.query.ExprColumn; -import org.labkey.api.security.User; -import org.labkey.api.study.StudyService; -import org.labkey.study.model.StudyImpl; - -public class VisualizationVisitTagTable extends VirtualTable -{ - private final StudyImpl _study; - private final User _user; - private final boolean _useProtocolDay; - private final String _visitTagName; - private final String _dayString; - private final String _altQueryName; - - public VisualizationVisitTagTable(StudyQuerySchema schema, ContainerFilter cf, StudyImpl study, User user, String visitTagName, boolean useProtocolDay, String altQueryName) - { - super(schema.getDbSchema(), "VizVisitTag", schema, cf); - _study = study; - _user = user; - _visitTagName = visitTagName; - _useProtocolDay = useProtocolDay; - if (_useProtocolDay) - _dayString = "ProtocolDay"; - else - _dayString = "Day"; - _altQueryName = altQueryName; - - addColumn(new ExprColumn(this, StudyService.get().getSubjectColumnName(_study.getContainer()), - new SQLFragment(ExprColumn.STR_TABLE_ALIAS + "." + "ParticipantId"), JdbcType.VARCHAR)); - - // 20546: need to expose Container for use in StudyVisualizationProvider.getJoinColumns - addColumn(new ExprColumn(this, "Container", new SQLFragment(ExprColumn.STR_TABLE_ALIAS + "." + "Container"), JdbcType.VARCHAR)); - - addColumn(new ExprColumn(this, "ZeroDay", new SQLFragment(ExprColumn.STR_TABLE_ALIAS + "." + _dayString), JdbcType.INTEGER)); - } - - @NotNull - @Override - public SQLFragment getFromSQL(String alias) - { - checkReadBeforeExecute(); - if (_altQueryName == null) - { - String innerAlias = getSqlDialect().truncate(alias + "_SP", 0); - String joinString; - if (_useProtocolDay) - { - joinString = "\nJOIN study.Visit ON " + innerAlias + ".VisitId = study.Visit.RowId AND " + - innerAlias + ".VisitId IS NOT NULL"; - } - else - { - joinString = "\nJOIN study.ParticipantVisit ON " + innerAlias + ".VisitId = study.ParticipantVisit.VisitRowId AND " + - innerAlias + ".ParticipantId = study.ParticipantVisit.ParticipantId"; - } - - SQLFragment from = new SQLFragment("(SELECT VisitId, " + _dayString + ", " + innerAlias + ".ParticipantId, Container " + " FROM (SELECT ParticipantId, "); - from.append("COALESCE(CohortVisitTag.VisitId, NoCohortTag.VisitId) As VisitId, CurrentCohortId FROM study.Participant\n") - .append("LEFT OUTER JOIN study.VisitTagMap CohortVisitTag ON study.Participant.CurrentCohortId = CohortVisitTag.CohortID AND CohortVisitTag.VisitTag=") - .append("'" + _visitTagName + "'\n") - .append("AND CohortVisitTag.Container = Participant.Container"); - from.append("\nLEFT OUTER JOIN study.VisitTagMap NoCohortTag ON NoCohortTag.VisitTag =") - .append("'" + _visitTagName + "' AND NoCohortTag.CohortID IS NULL\n") - .append("AND NoCohortTag.Container = Participant.Container"); - from.append("\nWHERE "); - - // TODO: this is a temp fix for the Dataspace usecase - from.append(ContainerFilter.Type.AllInProject.create(_study.getContainer(), _user).getSQLFragment(getSchema(), new SQLFragment("study.Participant.Container"))); - - from.append(") ").appendIdentifier(innerAlias); - from.append(joinString); - from.append("\n) ").appendIdentifier(alias); - return from; - } - else - { - // allow caller to pass in their own query to use for the SQL to get the participant-to-zero day map (used for CDS study axis alignment by visit tag) - // NOTE: it is assumed that the query will have the expected columns plus a column for VisitTagMap to filter on - return new SQLFragment("(SELECT * FROM " + _altQueryName + " WHERE VisitTagName = '" + _visitTagName + "')").appendIdentifier(alias); - } - } -} diff --git a/study/src/org/labkey/study/visualization/StudyVisualizationProvider.java b/study/src/org/labkey/study/visualization/StudyVisualizationProvider.java index 445a8eb9fff..353feb1e7dc 100644 --- a/study/src/org/labkey/study/visualization/StudyVisualizationProvider.java +++ b/study/src/org/labkey/study/visualization/StudyVisualizationProvider.java @@ -177,31 +177,28 @@ public List> getJoinC joinCols.add(new Pair<>(firstSubjectCol, secondSubjectCol)); - if (!first.isVisitTagQuery() && ! second.isVisitTagQuery()) - { - // attempt to lookup the dataset using the queryName by label and then by name - Dataset firstDataset = StudyService.get().resolveDataset(first.getContainer(), first.getQueryName()); - Dataset secondDataset = StudyService.get().resolveDataset(second.getContainer(), second.getQueryName()); + // attempt to lookup the dataset using the queryName by label and then by name + Dataset firstDataset = StudyService.get().resolveDataset(first.getContainer(), first.getQueryName()); + Dataset secondDataset = StudyService.get().resolveDataset(second.getContainer(), second.getQueryName()); - boolean subjectJoinOnly = isGroupByQuery || first.isSkipVisitJoin() || second.isSkipVisitJoin(); + boolean subjectJoinOnly = isGroupByQuery || first.isSkipVisitJoin() || second.isSkipVisitJoin(); - // if either query is a demographic dataset, it's sufficient to join on subject only: - if (!subjectJoinOnly && (firstDataset == null || firstDataset.getKeyType() != Dataset.KeyType.SUBJECT) && - (secondDataset == null || secondDataset.getKeyType() != Dataset.KeyType.SUBJECT)) + // if either query is a demographic dataset, it's sufficient to join on subject only: + if (!subjectJoinOnly && (firstDataset == null || firstDataset.getKeyType() != Dataset.KeyType.SUBJECT) && + (secondDataset == null || secondDataset.getKeyType() != Dataset.KeyType.SUBJECT)) + { + VisualizationSourceColumn firstSequenceCol = getVisitJoinColumn(factory, first, firstSubjectNounSingular); + VisualizationSourceColumn secondSequenceCol = getVisitJoinColumn(factory, second, secondSubjectNounSingular); + joinCols.add(new Pair<>(firstSequenceCol, secondSequenceCol)); + + // for datasets with matching 3rd keys, join on subject/visit/key (if neither are pivoted), allowing null results for this column so as to follow the lead of the primary measure column for this query: + if (firstDataset != null && firstDataset.getKeyType() == Dataset.KeyType.SUBJECT_VISIT_OTHER && + secondDataset != null && secondDataset.getKeyType() == Dataset.KeyType.SUBJECT_VISIT_OTHER && + first.getPivot() == null && second.getPivot() == null && firstDataset.hasMatchingExtraKey(secondDataset)) { - VisualizationSourceColumn firstSequenceCol = getVisitJoinColumn(factory, first, firstSubjectNounSingular); - VisualizationSourceColumn secondSequenceCol = getVisitJoinColumn(factory, second, secondSubjectNounSingular); - joinCols.add(new Pair<>(firstSequenceCol, secondSequenceCol)); - - // for datasets with matching 3rd keys, join on subject/visit/key (if neither are pivoted), allowing null results for this column so as to follow the lead of the primary measure column for this query: - if (firstDataset != null && firstDataset.getKeyType() == Dataset.KeyType.SUBJECT_VISIT_OTHER && - secondDataset != null && secondDataset.getKeyType() == Dataset.KeyType.SUBJECT_VISIT_OTHER && - first.getPivot() == null && second.getPivot() == null && firstDataset.hasMatchingExtraKey(secondDataset)) - { - VisualizationSourceColumn firstKeyCol = factory.create(first.getSchema(), first.getQueryName(), firstDataset.getKeyPropertyName(), true); - VisualizationSourceColumn secondKeyCol = factory.create(second.getSchema(), second.getQueryName(), secondDataset.getKeyPropertyName(), true); - joinCols.add(new Pair<>(firstKeyCol, secondKeyCol)); - } + VisualizationSourceColumn firstKeyCol = factory.create(first.getSchema(), first.getQueryName(), firstDataset.getKeyPropertyName(), true); + VisualizationSourceColumn secondKeyCol = factory.create(second.getSchema(), second.getQueryName(), secondDataset.getKeyPropertyName(), true); + joinCols.add(new Pair<>(firstKeyCol, secondKeyCol)); } } diff --git a/visualization/src/org/labkey/visualization/sql/OuterJoinSourceQuery.java b/visualization/src/org/labkey/visualization/sql/OuterJoinSourceQuery.java index 5b4b214e07c..e955a3d1554 100644 --- a/visualization/src/org/labkey/visualization/sql/OuterJoinSourceQuery.java +++ b/visualization/src/org/labkey/visualization/sql/OuterJoinSourceQuery.java @@ -235,12 +235,6 @@ public boolean isSkipVisitJoin() return false; } - @Override - public boolean isVisitTagQuery() - { - return false; - } - @Override public boolean isRequireLeftJoin() { diff --git a/visualization/src/org/labkey/visualization/sql/VisualizationSourceQuery.java b/visualization/src/org/labkey/visualization/sql/VisualizationSourceQuery.java index ba0b0901c54..9827a6e2410 100644 --- a/visualization/src/org/labkey/visualization/sql/VisualizationSourceQuery.java +++ b/visualization/src/org/labkey/visualization/sql/VisualizationSourceQuery.java @@ -131,12 +131,6 @@ public boolean requireInnerJoin() return false; } - @Override - public boolean isVisitTagQuery() - { - return _queryName.startsWith("VisualizationVisitTag"); - } - @Override public boolean isRequireLeftJoin() { @@ -213,7 +207,7 @@ public String getSelectListName(Set selectAliases) private static void addToColMap(Map> colMap, String name, VisualizationSourceColumn alias) { - Set aliases = colMap.computeIfAbsent(name, k -> new LinkedHashSet<>()); + Set aliases = colMap.computeIfAbsent(name, n -> new LinkedHashSet<>()); aliases.add(alias); } @@ -434,7 +428,7 @@ public String getGroupByClause() return ""; } - private void appendValueList(StringBuilder sql, VisualizationSourceColumn col) throws org.labkey.api.visualization.SQLGenerationException + private void appendValueList(StringBuilder sql, VisualizationSourceColumn col) { if (col.getValues() != null && !col.getValues().isEmpty()) { @@ -457,7 +451,7 @@ private void appendValueList(StringBuilder sql, VisualizationSourceColumn col) t } } - public String getPivotClause() throws org.labkey.api.visualization.SQLGenerationException + public String getPivotClause() { if (_pivot != null) { @@ -560,7 +554,7 @@ private String appendSimpleFilter(StringBuilder where, SimpleFilter filter, Stri return separator; } - public String getWhereClause() throws org.labkey.api.visualization.SQLGenerationException + public String getWhereClause() { StringBuilder where = new StringBuilder(); String sep = "WHERE "; @@ -598,12 +592,11 @@ public String getWhereClause() throws org.labkey.api.visualization.SQLGeneration @Override public String getSQL(VisualizationSourceColumn.Factory factory) throws SQLGenerationException { - String sql = getSelectClause(factory) + "\n" + + return getSelectClause(factory) + "\n" + getFromClause() + "\n" + getWhereClause() + "\n" + getGroupByClause() + "\n" + getPivotClause() + "\n"; - return sql; } @Override From f689c93f3c60ed79720985a3a42c4775cbb26507 Mon Sep 17 00:00:00 2001 From: Josh Eckels Date: Fri, 22 May 2026 18:34:35 -0700 Subject: [PATCH 5/5] Prohibit HTTP GET parameters for jsessionid (#7676) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### Rationale Servers that run both HTTP and HTTPS can end up choosing to send `jsessionid` values as GET parameters, because they may have an HTTP cookie that set to `Secure`. In scenarios like this, we want to be sure that we end up redirecting the client to HTTPS. We don't want session IDs to ever leak onto the URL. #### Changes - Tell Tomcat to only use cookies for communicating sessions #### Tasks 📍 - [x] Claude Code Review - [x] Manual Testing @labkey-tchad - Configure HTTPS via `application.properties` - Enable a separate HTTP port - Don't have HTTP->HTTPS redirect enabled in Site Settings - Log in via HTTPS - Hit the server via HTTP - Ensure you don't see a session on the URL - ~Test Automation~ - ~Verify Fix~ --- api/webapp/WEB-INF/web.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/api/webapp/WEB-INF/web.xml b/api/webapp/WEB-INF/web.xml index 3dbc079f803..450a46349cd 100755 --- a/api/webapp/WEB-INF/web.xml +++ b/api/webapp/WEB-INF/web.xml @@ -58,6 +58,11 @@ *.post + + + COOKIE + + /