fix(mysql): modifies hashing and equals behavior for primary keys to match mysql behavior (#13984)

This commit is contained in:
RyanHolstien 2025-07-08 10:12:23 -05:00 committed by GitHub
parent 1228f9b1de
commit c2928a1a6d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 66 additions and 5 deletions

View File

@ -303,14 +303,15 @@ public class EbeanAspectDao implements AspectDao, AspectMigrationsDao {
int position = 0;
List<EbeanAspectV2.PrimaryKey> keyList = new ArrayList<>(keys);
final int totalPageCount = QueryUtils.getTotalPageCount(keys.size(), keysCount);
final List<EbeanAspectV2> finalResult =
batchGetSelectString(new ArrayList<>(keys), keysCount, position, forUpdate);
batchGetSelectString(keyList, keysCount, position, forUpdate);
while (QueryUtils.hasMore(position, keysCount, totalPageCount)) {
position += keysCount;
final List<EbeanAspectV2> oneStatementResult =
batchGetSelectString(new ArrayList<>(keys), keysCount, position, forUpdate);
batchGetSelectString(keyList, keysCount, position, forUpdate);
finalResult.addAll(oneStatementResult);
}

View File

@ -12,9 +12,9 @@ import jakarta.persistence.Lob;
import jakarta.persistence.Table;
import java.io.Serializable;
import java.sql.Timestamp;
import java.util.Objects;
import javax.annotation.Nonnull;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@ -43,13 +43,17 @@ public class EbeanAspectV2 extends Model {
/** Key for an aspect in the table. */
@Embeddable
@Getter
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode
public static class PrimaryKey implements Serializable {
private static final long serialVersionUID = 1L;
public PrimaryKey(@Nonnull String urn, @Nonnull String aspect, long version) {
this.urn = urn.stripTrailing();
this.aspect = aspect.stripTrailing();
this.version = version;
}
@Nonnull
@Index
@Column(name = URN_COLUMN, length = 500, nullable = false)
@ -71,6 +75,27 @@ public class EbeanAspectV2 extends Model {
public EntityAspectIdentifier toAspectIdentifier() {
return new EntityAspectIdentifier(getUrn(), getAspect(), getVersion());
}
// Custom Equals and Hash code that trims to handle MySQL PAD SPACE:
// https://dev.mysql.com/doc/refman/8.4/en/charset-binary-collations.html#charset-binary-collations-trailing-space-comparisons
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
PrimaryKey that = (PrimaryKey) o;
return version == that.version
&& Objects.equals(urn.stripTrailing(), that.urn.stripTrailing())
&& Objects.equals(aspect.stripTrailing(), that.aspect.stripTrailing());
}
@Override
public int hashCode() {
return Objects.hash(urn.stripTrailing(), aspect.stripTrailing(), version);
}
}
@Nonnull @EmbeddedId @Index protected PrimaryKey key;

View File

@ -70,6 +70,7 @@ import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.apache.commons.lang3.tuple.Triple;
import org.testcontainers.shaded.com.google.common.collect.ImmutableSet;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
@ -482,6 +483,40 @@ public class EbeanEntityServiceTest
"Expected version 0 with systemMeta version 3 accounting for the the collision");
}
// NOTE: This is not currently super useful because H2 always treats spaces as significant
@Test
public void testSystemMetadataDuplicateKeyWhitespace() throws Exception {
Urn entityUrn = UrnUtils.getUrn("urn:li:corpuser:duplicateKeyTest");
SystemMetadata systemMetadata = AspectGenerationUtils.createSystemMetadata();
ChangeItemImpl item =
ChangeItemImpl.builder()
.urn(entityUrn)
.aspectName(STATUS_ASPECT_NAME)
.recordTemplate(new Status().setRemoved(true))
.systemMetadata(systemMetadata)
.auditStamp(TEST_AUDIT_STAMP)
.build(TestOperationContexts.emptyActiveUsersAspectRetriever(null));
_entityServiceImpl.ingestAspects(
opContext,
AspectsBatchImpl.builder()
.retrieverContext(opContext.getRetrieverContext())
.items(List.of(item))
.build(opContext),
false,
true);
Urn entityUrnWhitespace = UrnUtils.getUrn(entityUrn + " ");
Map<Urn, List<EnvelopedAspect>> envelopedAspects =
_entityServiceImpl.getLatestEnvelopedAspects(
opContext,
ImmutableSet.of(entityUrn, entityUrnWhitespace),
ImmutableSet.of(STATUS_ASPECT_NAME),
false);
assertEquals(envelopedAspects.get(entityUrn).size(), 1);
assertEquals(envelopedAspects.get(entityUrnWhitespace).size(), 0);
}
@Test
public void dataGeneratorThreadingTest() {
DataGenerator dataGenerator = new DataGenerator(opContext, _entityServiceImpl);