Skip to content

Issue 52657: LKSM: We shouldn't allow creating sample names that differ only in case. #6820

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions api/src/org/labkey/api/exp/api/ExperimentService.java
Original file line number Diff line number Diff line change
Expand Up @@ -1155,6 +1155,8 @@ List<? extends ExpProtocol> getExpProtocolsWithParameterValue(

Map<String, Map<String, Object>> getDomainMetrics();

boolean checkDuplicateName(@NotNull String newName, @NotNull TableInfo tableInfo);

class XarExportOptions
{
String _lsidRelativizer = LSID_OPTION_FOLDER_RELATIVE;
Expand Down
75 changes: 74 additions & 1 deletion experiment/src/org/labkey/experiment/ExpDataIterators.java
Original file line number Diff line number Diff line change
Expand Up @@ -2370,9 +2370,15 @@ else if (isMergeOrUpdate)
step2c = LoggingDataIterator.wrap(new ExpDataIterators.SampleStatusCheckIteratorBuilder(step2b, _container));
}

DataIteratorBuilder step2d = step2c;
if (canUpdateNames && !dontUpdate.contains("name"))
{
step2d = LoggingDataIterator.wrap(new ExpDataIterators.DuplicateNameCheckIteratorBuilder(step2c, _propertiesTable));
}

// Insert into exp.data then the provisioned table
// Use embargo data iterator to ensure rows are committed before being sent along Issue 26082 (row at a time, reselect rowid)
DataIteratorBuilder step3 = LoggingDataIterator.wrap(new TableInsertDataIteratorBuilder(step2c, _expTable, _container)
DataIteratorBuilder step3 = LoggingDataIterator.wrap(new TableInsertDataIteratorBuilder(step2d, _expTable, _container)
.setKeyColumns(keyColumns)
.setDontUpdate(dontUpdate)
.setAddlSkipColumns(_excludedColumns)
Expand Down Expand Up @@ -3155,6 +3161,73 @@ public static void incrementCounts(Map<String, Integer> currentCounts, Map<Strin
});
}

public static class DuplicateNameCheckIteratorBuilder implements DataIteratorBuilder
{
private final DataIteratorBuilder _in;
private final TableInfo _tableInfo;

public DuplicateNameCheckIteratorBuilder(@NotNull DataIteratorBuilder in, TableInfo tableInfo)
{
_in = in;
_tableInfo = tableInfo;
}

@Override
public DataIterator getDataIterator(DataIteratorContext context)
{
DataIterator pre = _in.getDataIterator(context);
return LoggingDataIterator.wrap(new DuplicateNameCheckDataIterator(pre, context, _tableInfo));
}
}

public static class DuplicateNameCheckDataIterator extends WrapperDataIterator
{
final static String NAME_FIELD = "name";
private final DataIteratorContext _context;
private final Integer _nameCol;
private final TableInfo _tableInfo;

protected DuplicateNameCheckDataIterator(DataIterator di, DataIteratorContext context, TableInfo tableInfo)
{
super(di);
_context = context;
_tableInfo = tableInfo;
Map<String, Integer> map = DataIteratorUtil.createColumnNameMap(di);
_nameCol = map.get(NAME_FIELD);
}

@Override
public boolean next() throws BatchValidationException
{
boolean hasNext = super.next();
if (!hasNext)
return false;

if (_context.getErrors().hasErrors())
return hasNext;

if (_nameCol == null)
return hasNext;

Object nameObj = get(_nameCol);
if (nameObj == null)
return hasNext;

String newName = String.valueOf(nameObj);
if (StringUtils.isEmpty(newName))
return hasNext;

Map<String, Object> existingValues = getExistingRecord();
if (existingValues != null && !existingValues.isEmpty() && existingValues.get(NAME_FIELD).equals(newName))
return hasNext;

if (ExperimentService.get().checkDuplicateName(newName, _tableInfo))
_context.getErrors().addRowError(new ValidationException(String.format("Name '%s' already exist.", newName)));

return hasNext;
}
}

public static class SampleStatusCheckIteratorBuilder implements DataIteratorBuilder
{
private final DataIteratorBuilder _in;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1323,6 +1323,11 @@ protected Map<String, Object> _update(User user, Container c, Map<String, Object
String newName = (String) row.get(Name.name());
String oldName = (String) oldRow.get(Name.name());
boolean hasNameChange = !StringUtils.isEmpty(newName) && !newName.equals(oldName);
if (hasNameChange)
{
if (ExperimentServiceImpl.get().checkDuplicateName(newName, _dataClass.getTinfo()))
throw new ValidationException(String.format("Name '%s' already exist.", newName));
}

// Replace attachment columns with filename and keep AttachmentFiles
Map<String, Object> rowStripped = new CaseInsensitiveHashMap<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9161,6 +9161,20 @@ public Map<String, Map<String, Object>> getDomainMetrics()
return metrics;
}

@Override
public boolean checkDuplicateName(@NotNull String newName, @NotNull TableInfo tableInfo)
{
SQLFragment dataRowSQL = new SQLFragment("SELECT name FROM ")
.append(tableInfo)
.append(" WHERE LOWER(name) = LOWER(?) AND name <> ?")
.add(newName)
.add(newName);
if (tableInfo.getSqlDialect().isSqlServer())
dataRowSQL.append(" COLLATE Latin1_General_BIN"); // force case sensitive comparison

return new SqlSelector(ExperimentService.get().getSchema(), dataRowSQL).exists();
}

private Map<String, Object> getImportTemplatesMetrics()
{
DbSchema dbSchema = CoreSchema.getInstance().getSchema();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@
import java.util.Optional;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.function.Function;
import java.util.function.Predicate;
Expand Down Expand Up @@ -1699,7 +1700,7 @@ private Map<Integer, Pair<Integer, String>> getSampleAliquotCounts(Collection<In
.append(")-1 AS CreatedAliquotCount FROM exp.material AS m WHERE m.rowid\s");
dialect.appendInClauseSql(sql, sampleIds);

Map<Integer, Pair<Integer, String>> sampleAliquotCounts = new HashMap<>();
Map<Integer, Pair<Integer, String>> sampleAliquotCounts = new TreeMap<>(); // Order sample by rowId to reduce probability of deadlock with search indexer
try (ResultSet rs = new SqlSelector(dbSchema, sql).getResultSet())
{
while (rs.next())
Expand Down Expand Up @@ -1760,7 +1761,7 @@ SELECT RootMaterialRowId as rootRowId, COUNT(*) as aliquotCount
}
dialect.appendInClauseSql(sql, sampleIds);

Map<Integer, Pair<Integer, String>> sampleAliquotCounts = new HashMap<>();
Map<Integer, Pair<Integer, String>> sampleAliquotCounts = new TreeMap<>(); // Order by rowId to reduce deadlock with search indexer
try (ResultSet rs = new SqlSelector(dbSchema, sql).getResultSet())
{
while (rs.next())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,12 @@ protected Map<String, Object> _update(User user, Container c, Map<String, Object
if (hasNameChange && !NameExpressionOptionService.get().getAllowUserSpecificNamesValue(c))
throw new ValidationException("User-specified sample name not allowed");

if (hasNameChange)
{
if (ExperimentServiceImpl.get().checkDuplicateName(newName, _sampleType.getTinfo()))
throw new ValidationException(String.format("Name '%s' already exist.", newName));
}

String oldAliquotedFromLSID = (String) oldRow.get(AliquotedFromLSID.name());
boolean isAliquot = !StringUtils.isEmpty(oldAliquotedFromLSID);

Expand Down