Skip to content

Commit 9179d1b

Browse files
committed
add null primary key validator
Null primary keys are not supported. Add a validator which can be used to detect when a record with a null primary key field is produced.
1 parent da9ef6c commit 9179d1b

File tree

2 files changed

+374
-0
lines changed

2 files changed

+374
-0
lines changed
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package com.netflix.hollow.api.producer.validation;
2+
3+
import com.netflix.hollow.api.producer.HollowProducer;
4+
import com.netflix.hollow.core.index.key.PrimaryKey;
5+
import com.netflix.hollow.core.read.HollowReadFieldUtils;
6+
import com.netflix.hollow.core.read.engine.HollowReadStateEngine;
7+
import com.netflix.hollow.core.read.engine.object.HollowObjectTypeReadState;
8+
import com.netflix.hollow.core.schema.HollowObjectSchema;
9+
import com.netflix.hollow.core.schema.HollowSchema;
10+
import com.netflix.hollow.core.write.objectmapper.HollowPrimaryKey;
11+
import com.netflix.hollow.core.write.objectmapper.HollowTypeMapper;
12+
13+
import java.util.Arrays;
14+
import java.util.BitSet;
15+
import java.util.HashMap;
16+
import java.util.Map;
17+
import java.util.Objects;
18+
import java.util.stream.Collectors;
19+
20+
import static com.netflix.hollow.core.HollowConstants.ORDINAL_NONE;
21+
22+
/**
23+
* A validator that fails if any records of a given type have null primary key fields.
24+
* <p>
25+
* The primary key may be declared on a data model class using the {@link HollowPrimaryKey} annotation, may be
26+
* declared more directly using {@link PrimaryKey} with {@link HollowObjectSchema}, or declared explicitly when
27+
* instantiating this validator.
28+
*/
29+
public class NullPrimaryKeyFieldValidator implements ValidatorListener {
30+
private static final String NAME = NullPrimaryKeyFieldValidator.class.getName();
31+
private static final String NULL_PRIMARY_KEYS_FOUND_ERROR_MSG_FORMAT =
32+
"Null primary key fields found for type %s. Primary Key in schema is %s. "
33+
+ "Null records: [%s]";
34+
private static final String NO_PRIMARY_KEY_ERROR_MSG_FORMAT =
35+
"NullPrimaryKeyFieldValidator defined but unable to find primary key for data type %s. "
36+
+ "Please check schema definition.";
37+
38+
private static final String NO_SCHEMA_FOUND_MSG_FORMAT =
39+
"NullPrimaryKeyFieldValidator defined for data type %s but schema not found. "
40+
+ "Please check that the HollowProducer is initialized with "
41+
+ "the data type's schema (see initializeDataModel)";
42+
private static final String NOT_AN_OBJECT_ERROR_MSG_FORMAT =
43+
"NullPrimaryKeyFieldValidator is defined but schema type of %s is not Object. "
44+
+ "This validation cannot be done.";
45+
46+
private static final String FIELD_PATH_NAME = "FieldPaths";
47+
private static final String DATA_TYPE_NAME = "Typename";
48+
49+
private final String dataTypeName;
50+
51+
/**
52+
* Creates a validator to detect records with null primary key fields for the type
53+
* that corresponds to the given data type class annotated with {@link HollowPrimaryKey}.
54+
*
55+
* @param dataType the data type class
56+
* @throws IllegalArgumentException if the data type class is not annotated with {@link HollowPrimaryKey}
57+
*/
58+
public NullPrimaryKeyFieldValidator(Class<?> dataType) {
59+
Objects.requireNonNull(dataType);
60+
61+
if (!dataType.isAnnotationPresent(HollowPrimaryKey.class)) {
62+
throw new IllegalArgumentException("The data class " +
63+
dataType.getName() +
64+
" must be annotated with @HollowPrimaryKey");
65+
}
66+
67+
this.dataTypeName = HollowTypeMapper.getDefaultTypeName(dataType);
68+
}
69+
70+
/**
71+
* Creates a validator to detect records with null primary keys of the type
72+
* that corresponds to the given data type class annotated with {@link HollowPrimaryKey}.
73+
* <p>
74+
* The validator will fail, when {@link #onValidate validating}, if a schema with a
75+
* primary key definition does not exist for the given data type name.
76+
*
77+
* @param dataTypeName the data type name
78+
*/
79+
public NullPrimaryKeyFieldValidator(String dataTypeName) {
80+
this.dataTypeName = Objects.requireNonNull(dataTypeName);
81+
}
82+
83+
@Override
84+
public String getName() {
85+
return NAME + "_" + dataTypeName;
86+
}
87+
88+
@Override
89+
public ValidationResult onValidate(HollowProducer.ReadState readState) {
90+
ValidationResult.ValidationResultBuilder vrb = ValidationResult.from(this);
91+
vrb.detail(DATA_TYPE_NAME, dataTypeName);
92+
93+
PrimaryKey primaryKey;
94+
HollowSchema schema = readState.getStateEngine().getSchema(dataTypeName);
95+
if (schema == null) {
96+
return vrb.failed(String.format(NO_SCHEMA_FOUND_MSG_FORMAT, dataTypeName));
97+
}
98+
if (schema.getSchemaType() != HollowSchema.SchemaType.OBJECT) {
99+
return vrb.failed(String.format(NOT_AN_OBJECT_ERROR_MSG_FORMAT, dataTypeName));
100+
}
101+
102+
HollowObjectSchema oSchema = (HollowObjectSchema) schema;
103+
primaryKey = oSchema.getPrimaryKey();
104+
if (primaryKey == null) {
105+
return vrb.failed(String.format(NO_PRIMARY_KEY_ERROR_MSG_FORMAT, dataTypeName));
106+
}
107+
108+
String fieldPaths = Arrays.toString(primaryKey.getFieldPaths());
109+
vrb.detail(FIELD_PATH_NAME, fieldPaths);
110+
111+
Map<Integer, Object[]> ordinalToNullPkey = getNullPrimaryKeyValues(readState, primaryKey);
112+
if (!ordinalToNullPkey.isEmpty()) {
113+
return vrb.failed(String.format(NULL_PRIMARY_KEYS_FOUND_ERROR_MSG_FORMAT,
114+
dataTypeName, fieldPaths,
115+
nullKeysToString(ordinalToNullPkey)));
116+
}
117+
118+
return vrb.passed(getName() + "no records with null primary key fields");
119+
}
120+
121+
private Map<Integer, Object[]> getNullPrimaryKeyValues(HollowProducer.ReadState readState, PrimaryKey primaryKey) {
122+
HollowReadStateEngine stateEngine = readState.getStateEngine();
123+
HollowObjectTypeReadState typeState = (HollowObjectTypeReadState) stateEngine.getTypeState(dataTypeName);
124+
125+
int[][] fieldPathIndexes = new int[primaryKey.getFieldPaths().length][];
126+
for (int i = 0; i < fieldPathIndexes.length; i++) {
127+
fieldPathIndexes[i] = primaryKey.getFieldPathIndex(stateEngine, i);
128+
}
129+
130+
BitSet ordinals = typeState.getPopulatedOrdinals();
131+
int ordinal = ordinals.nextSetBit(0);
132+
Map<Integer, Object[]> ordinalToNullPkey = new HashMap<>();
133+
while (ordinal != ORDINAL_NONE) {
134+
Object[] primaryKeyValues = getPrimaryKeyValue(typeState, fieldPathIndexes, ordinal);
135+
if (Arrays.stream(primaryKeyValues).anyMatch(Objects::isNull)) {
136+
ordinalToNullPkey.put(ordinal, primaryKeyValues);
137+
}
138+
ordinal = ordinals.nextSetBit(ordinal + 1);
139+
}
140+
return ordinalToNullPkey;
141+
}
142+
143+
private Object[] getPrimaryKeyValue(HollowObjectTypeReadState typeState, int[][] fieldPathIndexes, int ordinal) {
144+
Object[] results = new Object[fieldPathIndexes.length];
145+
for (int i = 0; i < fieldPathIndexes.length; i++) {
146+
results[i] = getPrimaryKeyFieldValue(typeState, fieldPathIndexes[i], ordinal);
147+
}
148+
149+
return results;
150+
}
151+
152+
private Object getPrimaryKeyFieldValue(HollowObjectTypeReadState typeState, int[] fieldPathIndexes, int ordinal) {
153+
HollowObjectSchema schema = typeState.getSchema();
154+
int lastFieldPath = fieldPathIndexes.length - 1;
155+
for (int i = 0; i < lastFieldPath; i++) {
156+
if (ordinal == ORDINAL_NONE) {
157+
// The ordinal must have referenced a null record.
158+
return null;
159+
}
160+
ordinal = typeState.readOrdinal(ordinal, fieldPathIndexes[i]);
161+
typeState = (HollowObjectTypeReadState) schema.getReferencedTypeState(fieldPathIndexes[i]);
162+
schema = typeState.getSchema();
163+
}
164+
165+
if (ordinal == ORDINAL_NONE) {
166+
// The ordinal must have referenced a record with a null value for this field.
167+
return null;
168+
}
169+
170+
return HollowReadFieldUtils.fieldValueObject(typeState, ordinal, fieldPathIndexes[lastFieldPath]);
171+
}
172+
173+
private String nullKeysToString(Map<Integer, Object[]> nullPrimaryKeyValues) {
174+
return nullPrimaryKeyValues.entrySet().stream()
175+
.map(entry -> {
176+
return "(ordinal=" + entry.getKey() + ", key=" + Arrays.toString(entry.getValue()) + ")";
177+
})
178+
.collect(Collectors.joining(", "));
179+
}
180+
}
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
package com.netflix.hollow.api.producer.validation;
2+
3+
import com.netflix.hollow.api.producer.HollowProducer;
4+
import com.netflix.hollow.api.producer.fs.HollowInMemoryBlobStager;
5+
import com.netflix.hollow.core.write.objectmapper.HollowPrimaryKey;
6+
import com.netflix.hollow.test.InMemoryBlobStore;
7+
import org.junit.Before;
8+
import org.junit.Test;
9+
10+
import static org.junit.Assert.*;
11+
12+
public class NullPrimaryKeyFieldValidatorTest {
13+
private InMemoryBlobStore blobStore;
14+
15+
@Before
16+
public void setUp() {
17+
blobStore = new InMemoryBlobStore();
18+
}
19+
20+
@Test
21+
public void testValidPrimaryKey() {
22+
HollowProducer producer = HollowProducer.withPublisher(blobStore)
23+
.withBlobStager(new HollowInMemoryBlobStager())
24+
.withListener(new NullPrimaryKeyFieldValidator(TypeWithSinglePrimaryKey.class)).build();
25+
try {
26+
producer.runCycle(state -> {
27+
TypeWithSinglePrimaryKey nullSinglePrimaryKey = new TypeWithSinglePrimaryKey(1);
28+
state.add(nullSinglePrimaryKey);
29+
});
30+
} catch (Exception e) {
31+
fail();
32+
}
33+
}
34+
35+
@Test
36+
public void testInvalidNoSchema() {
37+
HollowProducer producer = HollowProducer.withPublisher(blobStore)
38+
.withBlobStager(new HollowInMemoryBlobStager())
39+
.withListener(new NullPrimaryKeyFieldValidator(TypeWithSinglePrimaryKey.class)).build();
40+
try {
41+
producer.runCycle(state -> {
42+
state.add("hello");
43+
});
44+
fail();
45+
} catch (Exception e) {
46+
assertTrue(e instanceof ValidationStatusException);
47+
ValidationStatusException expected = (ValidationStatusException) e;
48+
assertEquals(1, expected.getValidationStatus().getResults().size());
49+
assertEquals("NullPrimaryKeyFieldValidator defined for data type TypeWithSinglePrimaryKey "+
50+
"but schema not found. Please check that the HollowProducer is initialized "+
51+
"with the data type's schema (see initializeDataModel)",
52+
expected.getValidationStatus().getResults().get(0).getMessage());
53+
}
54+
}
55+
56+
@Test
57+
public void testInvalidNoPrimaryKey() {
58+
HollowProducer producer = HollowProducer.withPublisher(blobStore)
59+
.withBlobStager(new HollowInMemoryBlobStager())
60+
.withListener(new NullPrimaryKeyFieldValidator("TypeWithoutPrimaryKey")).build();
61+
try {
62+
producer.runCycle(state -> {
63+
state.add(new TypeWithoutPrimaryKey(1));
64+
});
65+
fail();
66+
} catch (Exception e) {
67+
assertTrue(e instanceof ValidationStatusException);
68+
ValidationStatusException expected = (ValidationStatusException) e;
69+
assertEquals(1, expected.getValidationStatus().getResults().size());
70+
assertEquals("NullPrimaryKeyFieldValidator defined but unable to find primary key "+
71+
"for data type TypeWithoutPrimaryKey. Please check schema definition.",
72+
expected.getValidationStatus().getResults().get(0).getMessage());
73+
}
74+
}
75+
76+
@Test
77+
public void testInvalidNullSinglePrimaryKey() {
78+
HollowProducer producer = HollowProducer.withPublisher(blobStore)
79+
.withBlobStager(new HollowInMemoryBlobStager())
80+
.withListener(new NullPrimaryKeyFieldValidator(TypeWithSinglePrimaryKey.class)).build();
81+
try {
82+
producer.runCycle(state -> {
83+
TypeWithSinglePrimaryKey nullSinglePrimaryKey = new TypeWithSinglePrimaryKey(null);
84+
state.add(nullSinglePrimaryKey);
85+
});
86+
fail();
87+
} catch (Exception e) {
88+
assertTrue(e instanceof ValidationStatusException);
89+
ValidationStatusException expected = (ValidationStatusException) e;
90+
assertEquals(1, expected.getValidationStatus().getResults().size());
91+
assertEquals("Null primary key fields found for type TypeWithSinglePrimaryKey. "+
92+
"Primary Key in schema is [id]. Null records: [(ordinal=0, key=[null])]",
93+
expected.getValidationStatus().getResults().get(0).getMessage());
94+
}
95+
}
96+
97+
@Test
98+
public void testInvalidNullMultiPrimaryKeys() {
99+
HollowProducer producer = HollowProducer.withPublisher(blobStore)
100+
.withBlobStager(new HollowInMemoryBlobStager())
101+
.withListener(new NullPrimaryKeyFieldValidator(TypeWithMultiplePrimaryKeys.class)).build();
102+
try {
103+
producer.runCycle(state -> {
104+
TypeWithMultiplePrimaryKeys nullMultiPrimaryKey = new TypeWithMultiplePrimaryKeys(1, null);
105+
state.add(nullMultiPrimaryKey);
106+
});
107+
fail();
108+
} catch (Exception e) {
109+
assertTrue(e instanceof ValidationStatusException);
110+
ValidationStatusException expected = (ValidationStatusException) e;
111+
assertEquals(1, expected.getValidationStatus().getResults().size());
112+
assertEquals("Null primary key fields found for type TypeWithMultiplePrimaryKeys. "+
113+
"Primary Key in schema is [id, name]. Null records: [(ordinal=0, key=[1, null])]",
114+
expected.getValidationStatus().getResults().get(0).getMessage());
115+
}
116+
}
117+
118+
@Test
119+
public void testInvalidNullNestedPrimaryKey() {
120+
HollowProducer producer = HollowProducer.withPublisher(blobStore)
121+
.withBlobStager(new HollowInMemoryBlobStager())
122+
.withListener(new NullPrimaryKeyFieldValidator(TypeWithNestedPrimaryKey.class)).build();
123+
124+
// Case where the referenced object itself is null.
125+
try {
126+
producer.runCycle(state -> {
127+
TypeWithNestedPrimaryKey nestedPrimaryKey = new TypeWithNestedPrimaryKey(null);
128+
state.add(nestedPrimaryKey);
129+
});
130+
fail();
131+
} catch (Exception e) {
132+
assertTrue(e instanceof ValidationStatusException);
133+
ValidationStatusException expected = (ValidationStatusException) e;
134+
assertEquals(1, expected.getValidationStatus().getResults().size());
135+
assertEquals("Null primary key fields found for type TypeWithNestedPrimaryKey. "+
136+
"Primary Key in schema is [nested.id]. Null records: [(ordinal=0, key=[null])]",
137+
expected.getValidationStatus().getResults().get(0).getMessage());
138+
}
139+
140+
// Case where the nested object key field is null.
141+
try {
142+
producer.runCycle(state -> {
143+
TypeWithNestedPrimaryKey nestedPrimaryKey = new TypeWithNestedPrimaryKey(new TypeWithoutPrimaryKey(null));
144+
state.add(nestedPrimaryKey);
145+
});
146+
fail();
147+
} catch (Exception e) {
148+
assertTrue(e instanceof ValidationStatusException);
149+
ValidationStatusException expected = (ValidationStatusException) e;
150+
assertEquals(1, expected.getValidationStatus().getResults().size());
151+
assertEquals("Null primary key fields found for type TypeWithNestedPrimaryKey. "+
152+
"Primary Key in schema is [nested.id]. Null records: [(ordinal=0, key=[null])]",
153+
expected.getValidationStatus().getResults().get(0).getMessage());
154+
}
155+
}
156+
157+
158+
@HollowPrimaryKey(fields = "id")
159+
static class TypeWithSinglePrimaryKey {
160+
private final Integer id;
161+
162+
public TypeWithSinglePrimaryKey(Integer id) {
163+
this.id = id;
164+
}
165+
}
166+
167+
@HollowPrimaryKey(fields = {"id", "name"})
168+
static class TypeWithMultiplePrimaryKeys {
169+
private final Integer id;
170+
private final String name;
171+
172+
public TypeWithMultiplePrimaryKeys(Integer id, String name) {
173+
this.id = id;
174+
this.name = name;
175+
}
176+
}
177+
178+
@HollowPrimaryKey(fields = {"nested.id"})
179+
static class TypeWithNestedPrimaryKey {
180+
private final TypeWithoutPrimaryKey nested;
181+
182+
public TypeWithNestedPrimaryKey(TypeWithoutPrimaryKey nested) {
183+
this.nested = nested;
184+
}
185+
}
186+
187+
static class TypeWithoutPrimaryKey {
188+
private final Integer id;
189+
190+
public TypeWithoutPrimaryKey(Integer id) {
191+
this.id = id;
192+
}
193+
}
194+
}

0 commit comments

Comments
 (0)