Skip to content
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package datawave.test.framework.generators.query;

import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;

/**
* Holds a generated query, its expected plan, and the event ids expected from execution.
*/
public class QueryMetadata {

private final String query;
private final String plan;
private final List<Integer> ids;

/**
* Static entry point
*
* @param query
* the query
* @param plan
* the query plan
* @param ids
* the list of result event ids
* @return the {@link QueryMetadata}
*/
public static QueryMetadata of(String query, String plan, List<Integer> ids) {
return new QueryMetadata(query, plan, ids);
}

/**
* Private constructor helps enforce static access
*
* @param query
* the query
* @param plan
* the query plan
* @param ids
* the result event ids
*/
private QueryMetadata(String query, String plan, List<Integer> ids) {
this.query = query;
this.plan = plan;
this.ids = ids;
}

public String getQuery() {
return query;
}

public String getPlan() {
return plan;
}

public List<Integer> getIds() {
return ids;
}

@Override
public String toString() {
return query;
}

/**
* Compares query and plan text and treats expected event ids as an unordered set.
*/
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) {
return false;
}
QueryMetadata metadata = (QueryMetadata) o;
return Objects.equals(query, metadata.query) && Objects.equals(plan, metadata.plan) && Objects.equals(idSet(), metadata.idSet());
}

@Override
public int hashCode() {
return Objects.hash(query, plan, idSet());
}

/**
* The expected event ids as a set, so equality does not depend on their order.
*
* @return the ids as a set, or null when {@code ids} is null
*/
private Set<Integer> idSet() {
return ids == null ? null : new HashSet<>(ids);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package datawave.test.framework.generators.query.term;

import java.util.ArrayList;
import java.util.List;

import com.google.common.base.Preconditions;

import datawave.data.type.Type;
import datawave.test.framework.FieldMetadata;

/**
* Stores the result of the most recent field evaluation for {@link QueryTerm} implementations.
* <p>
* {@link QueryTerm#forEachEvaluation(FieldMetadata, Runnable)} sets {@link #metadata} before applying a term. Callers should read {@link #queryTerm},
* {@link #planTerm}, and {@link #eventIds} in the callback because the next application may replace them.
*/
public abstract class AbstractQueryTerm implements QueryTerm {

protected FieldMetadata metadata;

protected String queryTerm;
protected String planTerm;
protected List<Integer> eventIds;

protected boolean isNegated = false;

/**
* Builds equality query and plan text for the value and records the event ids carrying that value.
* <p>
* When {@code negate} is true, the text is negated but {@link #eventIds} still holds the equality matches. Combining logic must determine the expected ids
* for the negated expression.
*
* @param value
* the value to build the equality term against
* @param negate
* true to wrap the term as {@code !(FIELD == 'value')}
*/
protected void applyEqualityTerm(String value, boolean negate) {
Preconditions.checkNotNull(value, "Value cannot be null");
List<String> normalizedValues = createNormalizedValues(value);
if (normalizedValues.size() != 1) {
throw new RuntimeException("Expected a single normalized value but got: " + normalizedValues);
}
String normalizedValue = normalizedValues.get(0);

String eq = metadata.getNormalizedFieldName() + " == '" + value + "'";
String planEq = metadata.getNormalizedFieldName() + " == '" + normalizedValue + "'";
this.queryTerm = negate ? "!(" + eq + ")" : eq;
this.planTerm = negate ? "!(" + planEq + ")" : planEq;
this.eventIds = metadata.getEventIdsForValue(value);
}

@Override
public final List<String> createNormalizedValues(String value) {
List<String> normalizedValues = new ArrayList<>();
for (Type<?> type : metadata.getNormalizers()) {
String normalizedValue = type.normalize(value);
normalizedValues.add(normalizedValue);
}
return normalizedValues;
}

@Override
public final String getQueryTerm() {
return queryTerm;
}

@Override
public final String getPlanTerm() {
return planTerm;
}

@Override
public final List<Integer> getEventIds() {
return eventIds;
}

@Override
public final boolean isNegated() {
return isNegated;
}

@Override
public final void setIsNegated(boolean isNegated) {
this.isNegated = isNegated;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package datawave.test.framework.generators.query.term;

import java.util.List;

import datawave.test.framework.FieldMetadata;

/**
* Produces query text, plan text, and event ids for a term applied to field metadata.
* <p>
* {@link #forEachEvaluation(FieldMetadata, Runnable)} invokes the callback after each application. The callback can read the current query text, plan text, and
* event ids through the getters; subsequent applications may replace them.
*/
public interface QueryTerm {

/**
* Applies this term to the given field once for every evaluation the field requires, running {@code onEvaluation} after each application so the caller can
* read the resulting {@link #getQueryTerm()}, {@link #getPlanTerm()} and {@link #getEventIds()}.
* <p>
* How many evaluations a field requires depends on the term: a {@link ValueDependentQueryTerm} is applied once per {@link FieldMetadata#getValues()} entry,
* while a {@link ValueIndependentQueryTerm} is applied exactly once per field because its text does not name a value.
*
* @param fieldMetadata
* the field to apply this term to
* @param onEvaluation
* run once after each application of this term
*/
void forEachEvaluation(FieldMetadata fieldMetadata, Runnable onEvaluation);

List<String> createNormalizedValues(String value);

String getQueryTerm();

String getPlanTerm();

List<Integer> getEventIds();

boolean isNegated();

void setIsNegated(boolean isNegated);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package datawave.test.framework.generators.query.term;

import datawave.test.framework.FieldMetadata;

/**
* Base class for {@link QueryTerm} implementations whose query and plan text name a specific field value, e.g. {@code FIELD == 'value'}.
* <p>
* Such a term is applied once per value the field carries, so every application receives a real value drawn from {@link FieldMetadata#getValues()}.
*/
public abstract class ValueDependentQueryTerm extends AbstractQueryTerm {

@Override
public final void forEachEvaluation(FieldMetadata fieldMetadata, Runnable onEvaluation) {
this.metadata = fieldMetadata;
for (String value : fieldMetadata.getValues()) {
apply(value);
onEvaluation.run();
}
}

/**
* Builds this term's query text, plan text and event ids against a single value of the current field.
*
* @param value
* the value to build this term against
*/
protected abstract void apply(String value);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package datawave.test.framework.generators.query.term;

import datawave.test.framework.FieldMetadata;

/**
* Base class for {@link QueryTerm} implementations whose query and plan text name only a field, never a value, e.g. the {@code filter:isNull} and
* {@code filter:isNotNull} filter functions.
* <p>
* Such a term is applied exactly once per field rather than once per value, and is never handed a value - real or placeholder - to ignore.
*/
public abstract class ValueIndependentQueryTerm extends AbstractQueryTerm {

@Override
public final void forEachEvaluation(FieldMetadata fieldMetadata, Runnable onEvaluation) {
this.metadata = fieldMetadata;
apply();
onEvaluation.run();
}

/**
* Builds this term's query text, plan text and event ids against the current field.
*/
protected abstract void apply();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package datawave.test.framework.generators.query;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;

import java.util.List;

import org.junit.jupiter.api.Test;

/**
* Tests {@link QueryMetadata} accessors and equality, including unordered comparison of expected event ids.
*/
class QueryMetadataTest {

@Test
void testAccessorsReturnWhatWasSupplied() {
QueryMetadata metadata = QueryMetadata.of("FIELD == 'a'", "FIELD == 'a'", List.of(1, 2));

assertEquals("FIELD == 'a'", metadata.getQuery());
assertEquals("FIELD == 'a'", metadata.getPlan());
assertEquals(List.of(1, 2), metadata.getIds());
}

/**
* The query text is what a failure report is read by, so it is what {@code toString} shows.
*/
@Test
void testToStringIsTheQuery() {
assertEquals("FIELD == 'a'", QueryMetadata.of("FIELD == 'a'", "FIELD == 'plan'", List.of(1)).toString());
}

@Test
void testEqualWhenQueryPlanAndIdsMatch() {
QueryMetadata first = QueryMetadata.of("FIELD == 'a'", "FIELD == 'a'", List.of(1, 2));
QueryMetadata second = QueryMetadata.of("FIELD == 'a'", "FIELD == 'a'", List.of(1, 2));

assertEquals(first, second);
assertEquals(first.hashCode(), second.hashCode());
}

@Test
void testDifferingQueryIsNotEqual() {
QueryMetadata first = QueryMetadata.of("FIELD == 'a'", "FIELD == 'a'", List.of(1));
QueryMetadata second = QueryMetadata.of("FIELD == 'b'", "FIELD == 'a'", List.of(1));

assertNotEquals(first, second);
}

/**
* The plan is asserted separately from the query because a term can normalize into a different plan than the query text it came from.
*/
@Test
void testDifferingPlanIsNotEqual() {
QueryMetadata first = QueryMetadata.of("FIELD == 'Alpha'", "FIELD == 'alpha'", List.of(1));
QueryMetadata second = QueryMetadata.of("FIELD == 'Alpha'", "FIELD == 'Alpha'", List.of(1));

assertNotEquals(first, second);
}

/**
* Different expected event ids make query metadata unequal even when query and plan text match.
*/
@Test
void testDifferingIdsAreNotEqual() {
QueryMetadata first = QueryMetadata.of("FIELD == 'a'", "FIELD == 'a'", List.of(1, 2));
QueryMetadata second = QueryMetadata.of("FIELD == 'a'", "FIELD == 'a'", List.of(1, 2, 3));

assertNotEquals(first, second);
assertNotEquals(first.hashCode(), second.hashCode());
}

/**
* Event id order does not affect equality or the hash code.
*/
@Test
void testIdOrderDoesNotAffectEquality() {
QueryMetadata ascending = QueryMetadata.of("FIELD == 'a'", "FIELD == 'a'", List.of(1, 2, 3));
QueryMetadata shuffled = QueryMetadata.of("FIELD == 'a'", "FIELD == 'a'", List.of(3, 1, 2));

assertEquals(ascending, shuffled);
assertEquals(ascending.hashCode(), shuffled.hashCode());
}

@Test
void testNotEqualToNullOrAnotherType() {
QueryMetadata metadata = QueryMetadata.of("FIELD == 'a'", "FIELD == 'a'", List.of(1));

assertNotEquals(null, metadata);
assertNotEquals("FIELD == 'a'", metadata);
}

@Test
void testEmptyIdsAreDistinctFromPopulatedIds() {
QueryMetadata empty = QueryMetadata.of("FIELD == 'a'", "FIELD == 'a'", List.of());
QueryMetadata populated = QueryMetadata.of("FIELD == 'a'", "FIELD == 'a'", List.of(1));

assertNotEquals(empty, populated);
}
}
Loading
Loading