Skip to content
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

Extend MARC-MARC search query to account for qualifiers #633

Merged
merged 12 commits into from
Jul 29, 2024
Merged
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* [MODSOURCE-773](https://folio-org.atlassian.net/browse/MODSOURCE-773) MARC Search omits suppressed from discovery records in default search
* [MODINV-1044](https://folio-org.atlassian.net/browse/MODINV-1044) Additional Requirements - Update Data Import logic to normalize OCLC 035 values
* [MODSOURMAN-1200](https://folio-org.atlassian.net/browse/MODSOURMAN-1200) Find record by match id on update generation
* [MODSOURMAN-783](https://folio-org.atlassian.net/browse/MODSOURCE-783) Extend MARC-MARC search query to account for qualifiers

## 2024-03-20 5.8.0
* [MODSOURCE-733](https://issues.folio.org/browse/MODSOURCE-733) Reduce Memory Allocation of Strings
Expand Down
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you please also replace this line import static org.jooq.impl.DSL.*; with exact importing classes?

Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,9 @@ public class RecordDaoImpl implements RecordDao {
static final int INDEXERS_DELETION_LOCK_NAMESPACE_ID = "delete_marc_indexers".hashCode();

public static final String CONTROL_FIELD_CONDITION_TEMPLATE = "\"{partition}\".\"value\" in ({value})";
public static final String CONTROL_FIELD_CONDITION_TEMPLATE_WITH_QUALIFIER = "\"{partition}\".\"value\" IN ({value}) AND \"{partition}\".\"value\" LIKE {qualifier}";
public static final String DATA_FIELD_CONDITION_TEMPLATE = "\"{partition}\".\"value\" in ({value}) and \"{partition}\".\"ind1\" LIKE '{ind1}' and \"{partition}\".\"ind2\" LIKE '{ind2}' and \"{partition}\".\"subfield_no\" = '{subfield}'";
public static final String DATA_FIELD_CONDITION_TEMPLATE_WITH_QUALIFIER = "\"{partition}\".\"value\" IN ({value}) AND \"{partition}\".\"value\" LIKE {qualifier} AND \"{partition}\".\"ind1\" LIKE '{ind1}' AND \"{partition}\".\"ind2\" LIKE '{ind2}' AND \"{partition}\".\"subfield_no\" = '{subfield}'";
private static final String VALUE_IN_SINGLE_QUOTES = "'%s'";
private static final String RECORD_NOT_FOUND_BY_ID_TYPE = "Record with %s id: %s was not found";
private static final String INVALID_PARSED_RECORD_MESSAGE_TEMPLATE = "Record %s has invalid parsed record; %s";
Expand Down Expand Up @@ -352,18 +354,26 @@ public Future<List<Record>> getMatchedRecordsWithoutIndexersVersionUsage(MatchFi

private Condition getMatchedFieldCondition(MatchField matchedField, String partition) {
Map<String, String> params = new HashMap<>();
var qualifierSearch = false;
params.put("partition", partition);
params.put("value", getValueInSqlFormat(matchedField.getValue()));
if (matchedField.getQualifierMatch() != null) {
qualifierSearch = true;
params.put("qualifier", getSqlQualifier(matchedField.getQualifierMatch()));
}
String sql;
Condition condition;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if condition is not used, let's remove it

if (matchedField.isControlField()) {
String sql = StrSubstitutor.replace(CONTROL_FIELD_CONDITION_TEMPLATE, params, "{", "}");
return condition(sql);
sql = qualifierSearch ? StrSubstitutor.replace(CONTROL_FIELD_CONDITION_TEMPLATE_WITH_QUALIFIER, params, "{", "}")
: StrSubstitutor.replace(CONTROL_FIELD_CONDITION_TEMPLATE, params, "{", "}");
} else {
params.put("ind1", getSqlInd(matchedField.getInd1()));
params.put("ind2", getSqlInd(matchedField.getInd2()));
params.put("subfield", matchedField.getSubfield());
String sql = StrSubstitutor.replace(DATA_FIELD_CONDITION_TEMPLATE, params, "{", "}");
return condition(sql);
sql = qualifierSearch ? sql = StrSubstitutor.replace(DATA_FIELD_CONDITION_TEMPLATE_WITH_QUALIFIER, params, "{", "}")
: StrSubstitutor.replace(DATA_FIELD_CONDITION_TEMPLATE, params, "{", "}");
}
return condition(sql);
}

private String getSqlInd(String ind) {
Expand All @@ -372,6 +382,19 @@ private String getSqlInd(String ind) {
return ind;
}

private String getSqlQualifier(MatchField.QualifierMatch qualifierMatch) {
if (qualifierMatch == null) {
return null;
}
var value = qualifierMatch.value();

return switch (qualifierMatch.qualifier()) {
case BEGINS_WITH -> "'" + value + "%'";
case ENDS_WITH -> "'%" + value + "'";
case CONTAINS -> "'%" + value + "%'";
};
}

private String getValueInSqlFormat(Value value) {
if (Value.ValueType.STRING.equals(value.getType())) {
return format(VALUE_IN_SINGLE_QUOTES, value.getValue());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.folio.dao.util;

import org.folio.processing.value.Value;
import org.folio.rest.jaxrs.model.Filter;
import org.marc4j.marc.impl.Verifier;

/**
Expand All @@ -15,6 +16,7 @@ public class MatchField {
private final String ind2;
private final String subfield;
private final Value value;
private final QualifierMatch qualifierMatch;
private final String fieldPath;

public MatchField(String tag, String ind1, String ind2, String subfield, Value value) {
Expand All @@ -24,6 +26,20 @@ public MatchField(String tag, String ind1, String ind2, String subfield, Value v
this.subfield = subfield;
this.value = value;
this.fieldPath = tag + ind1 + ind2 + subfield;
this.qualifierMatch = null;
}

public MatchField(String tag, String ind1, String ind2, String subfield, Value value, QualifierMatch qualifierMatch) {
this.tag = tag;
this.ind1 = ind1;
this.ind2 = ind2;
this.subfield = subfield;
this.value = value;
this.qualifierMatch = qualifierMatch;
this.fieldPath = tag + ind1 + ind2 + subfield;
}

public record QualifierMatch(Filter.Qualifier qualifier, String value) {
}

public String getTag() {
Expand All @@ -46,6 +62,10 @@ public Value getValue() {
return value;
}

public QualifierMatch getQualifierMatch() {
return qualifierMatch;
}

public boolean isControlField() {
return Verifier.isControlField(tag);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public final class RecordDaoUtil {
public static final String RECORD_NOT_FOUND_TEMPLATE = "Record with id '%s' was not found";

private static final String COMMA = ",";
private static final String LIKE_OPERATOR = "%";
private static final List<String> DELETED_LEADER_RECORD_STATUS = Arrays.asList("d", "s", "x");

private RecordDaoUtil() {}
Expand All @@ -77,6 +78,18 @@ public static Condition getExternalIdsCondition(List<String> externalIds, IdType
return getIdCondition(idType, idField -> idField.in(toUUIDs(externalIds)));
}

/**
* Get {@link Condition} where in external list ids and {@link IdType} and match by qualifier value
*
* @param externalIds list of external id
* @param idType external id type
* @param qualifier qualifier type and value
* @return condition
*/
public static Condition getExternalIdsConditionWithQualifier(List<String> externalIds, IdType idType, MatchField.QualifierMatch qualifier) {
return getIdConditionWithQualifier(idType, idField -> idField.in(toUUIDs(externalIds)), qualifier);
}

/**
* Count query by {@link Condition}
*
Expand Down Expand Up @@ -478,6 +491,19 @@ public static Condition filterRecordByExternalHridValues(List<String> externalHr
return RECORDS_LB.EXTERNAL_HRID.in(externalHridValues);
}

/**
* Get {@link Condition} to filter by external entity hrid using specified values and match by qualifier value
*
* @param externalHridValues external entity hrid values to equal
* @param qualifier qualifier type and value
* @return condition
*/
public static Condition filterRecordByExternalHridValuesWithQualifier(List<String> externalHridValues, MatchField.QualifierMatch qualifier) {
var qualifierCondition = buildQualifierCondition(RECORDS_LB.EXTERNAL_HRID, qualifier);
var resultCondition = RECORDS_LB.EXTERNAL_HRID.in(externalHridValues);
return qualifierCondition != null ? resultCondition.and(qualifierCondition) : resultCondition;
}

/**
* Get {@link Condition} to filter by snapshotId id
*
Expand Down Expand Up @@ -734,20 +760,47 @@ private static Condition getRecordTypeCondition(RecordType recordType) {
}

private static Condition getIdCondition(IdType idType, Function<Field<UUID>, Condition> idFieldToConditionMapper) {
IdType idTypeToUse = idType;
RecordType recordType = null;
if (idType == IdType.HOLDINGS) {
idTypeToUse = IdType.EXTERNAL;
recordType = RecordType.MARC_HOLDING;
} else if (idType == IdType.INSTANCE) {
idTypeToUse = IdType.EXTERNAL;
recordType = RecordType.MARC_BIB;
} else if (idType == IdType.AUTHORITY) {
idTypeToUse = IdType.EXTERNAL;
recordType = RecordType.MARC_AUTHORITY;
}
IdType idTypeToUse = getIdType(idType);
RecordType recordType = getRecordType(idTypeToUse);
var idField = RECORDS_LB.field(LOWER_CAMEL.to(LOWER_UNDERSCORE, idTypeToUse.getIdField()), UUID.class);
return idFieldToConditionMapper.apply(idField).and(getRecordTypeCondition(recordType));
}

private static Condition getIdConditionWithQualifier(IdType idType, Function<Field<UUID>, Condition> idFieldToConditionMapper, MatchField.QualifierMatch qualifier) {
IdType idTypeToUse = getIdType(idType);
RecordType recordType = getRecordType(idType);
var idField = RECORDS_LB.field(LOWER_CAMEL.to(LOWER_UNDERSCORE, idTypeToUse.getIdField()), UUID.class);
var qualifierCondition = buildQualifierCondition(idField, qualifier);
var resultCondition = idFieldToConditionMapper.apply(idField).and(getRecordTypeCondition(recordType));
return qualifierCondition != null ? resultCondition.and(qualifierCondition) : resultCondition;
}

private static Condition buildQualifierCondition(Field field, MatchField.QualifierMatch qualifier) {
if (qualifier == null) {
return null;
}
var value = qualifier.value();
return switch (qualifier.qualifier()) {
case BEGINS_WITH -> field.like(value + LIKE_OPERATOR);
case ENDS_WITH -> field.like(LIKE_OPERATOR + value);
case CONTAINS -> field.like(LIKE_OPERATOR + value + LIKE_OPERATOR);
};
}

private static RecordType getRecordType(IdType idType) {
return switch (idType) {
case HOLDINGS -> RecordType.MARC_HOLDING;
case INSTANCE -> RecordType.MARC_BIB;
case AUTHORITY -> RecordType.MARC_AUTHORITY;
default -> null;
};
}

private static IdType getIdType(IdType idType) {
return switch (idType) {
case HOLDINGS, INSTANCE, AUTHORITY -> IdType.EXTERNAL;
default -> idType;
};
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
import static org.folio.dao.util.RecordDaoUtil.ensureRecordForeignKeys;
import static org.folio.dao.util.RecordDaoUtil.ensureRecordHasId;
import static org.folio.dao.util.RecordDaoUtil.ensureRecordHasSuppressDiscovery;
import static org.folio.dao.util.RecordDaoUtil.filterRecordByExternalHridValues;
import static org.folio.dao.util.RecordDaoUtil.filterRecordByExternalHridValuesWithQualifier;
import static org.folio.dao.util.RecordDaoUtil.filterRecordByState;
import static org.folio.dao.util.RecordDaoUtil.getExternalIdsCondition;
import static org.folio.dao.util.RecordDaoUtil.getExternalIdsConditionWithQualifier;
import static org.folio.dao.util.SnapshotDaoUtil.SNAPSHOT_NOT_FOUND_TEMPLATE;
import static org.folio.dao.util.SnapshotDaoUtil.SNAPSHOT_NOT_STARTED_MESSAGE_TEMPLATE;
import static org.folio.rest.util.QueryParamUtil.toRecordType;
Expand Down Expand Up @@ -462,7 +462,11 @@ private MatchField prepareMatchField(RecordMatchingDto recordMatchingDto) {
String ind1 = filter.getIndicator1() != null ? filter.getIndicator1() : StringUtils.EMPTY;
String ind2 = filter.getIndicator2() != null ? filter.getIndicator2() : StringUtils.EMPTY;
String subfield = filter.getSubfield() != null ? filter.getSubfield() : StringUtils.EMPTY;
return new MatchField(filter.getField(), ind1, ind2, subfield, ListValue.of(filter.getValues()));
MatchField.QualifierMatch qualifier = null;
if (filter.getQualifier() != null && filter.getQualifierValue() != null) {
qualifier = new MatchField.QualifierMatch(filter.getQualifier(), filter.getQualifierValue());
}
return new MatchField(filter.getField(), ind1, ind2, subfield, ListValue.of(filter.getValues()), qualifier);
}

private TypeConnection getTypeConnection(RecordMatchingDto.RecordType recordType) {
Expand All @@ -477,13 +481,14 @@ private Future<RecordsIdentifiersCollection> processDefaultMatchField(MatchField
RecordMatchingDto recordMatchingDto, String tenantId) {
Condition condition = filterRecordByState(Record.State.ACTUAL.value());
List<String> values = ((ListValue) matchField.getValue()).getValue();
var qualifier = matchField.getQualifierMatch();

if (matchField.isMatchedId()) {
condition = condition.and(getExternalIdsCondition(values, IdType.RECORD));
condition = condition.and(getExternalIdsConditionWithQualifier(values, IdType.RECORD, qualifier));
} else if (matchField.isExternalId()) {
condition = condition.and(getExternalIdsCondition(values, IdType.EXTERNAL));
condition = condition.and(getExternalIdsConditionWithQualifier(values, IdType.EXTERNAL, qualifier));
} else if (matchField.isExternalHrid()) {
condition = condition.and(filterRecordByExternalHridValues(values));
condition = condition.and(filterRecordByExternalHridValuesWithQualifier(values, qualifier));
}

return recordDao.getRecords(condition, typeConnection.getDbType(), Collections.emptyList(), recordMatchingDto.getOffset(),
Expand Down
Loading
Loading