[common] Schema.PrimaryKey violates the equals/hashCode contract
- Dominant language
- Java
- Stars
- 2.1k
- Forks
- 625
- Avg merge
- 3d 14h
- Merged PRs (30d)
- 97
Description
### Search before asking
- [x] I searched in the [issues](https://github.com/apache/fluss/issues) and found nothing similar.
### Fluss version
0.9.0 (latest release)
### Please describe the bug 🐞
## Summary
`Schema.PrimaryKey.equals()` compares only the primary-key column names, but `hashCode()` folds in `super.hashCode()`:
```java
// fluss-common/src/main/java/org/apache/fluss/metadata/Schema.java
public boolean equals(Object o) {
...
return Objects.equals(columnNames, that.columnNames); // value-based
}
public int hashCode() {
return Objects.hash(super.hashCode(), columnNames); // includes Object.hashCode()
}
```
`PrimaryKey` extends only `Object`, so `super.hashCode()` is the per-instance `Object.hashCode()` value. Two `PrimaryKey` instances with the same columns are therefore `equals()` but return different hash codes, breaking the general contract of `Object.hashCode()`:
> "If two objects are equal according to the `equals(Object)` method, then calling the
> `hashCode` method on each of the two objects must produce the same integer result."
> — [`java.lang.Object.hashCode()`, Java SE 11 API](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/Object.html#hashCode())
`Schema.hashCode()` includes the primary key, so the violation propagates to `Schema`, and to the public `TableDescriptor`, whose `equals`/`hashCode` include the schema. `Schema` is `@PublicEvolving` and `PrimaryKey` is `@PublicStable`, so any caller that puts a schema or table descriptor into a `HashSet`/`HashMap` is affected. This is easy to hit after a schema round-trips through its JSON form (`Schema#toJsonBytes` / `fromJsonBytes`, which is how Fluss persists and reloads a schema, e.g. via `SchemaZNode`): the reloaded schema is `equals()` to the in-memory one but is not found in a hash-based collection.
## How to reproduce
```java
Schema schema = Schema.newBuilder()
.column("id", DataTypes.INT()).column("name", DataTypes.STRING())
.primaryKey("id").build();
Schema reloaded = Schema.fromJsonBytes(schema.toJsonBytes());
schema.equals(reloaded); // true
schema.hashCode() == reloaded.hashCode(); // false <- contract violation
Set set = new HashSet<>();
set.add(schema);
set.contains(reloaded); // false <- equal schema not found
```
Standalone runnable reproduction against the released `0.9.1-incubating` artifact (no cluster, no Fluss build needed): https://github.com/NestDream/fluss-pk-hashcode-demo. Run `./run-before.sh`; the core check is [SchemaHashCodeDemo.java#L30-L37](https://github.com/NestDream/fluss-pk-hashcode-demo/blob/main/src/main/java/io/github/nestdream/flussdemo/SchemaHashCodeDemo.java#L30-L37), with captured before/after logs under [`evidence/`](https://github.com/NestDream/fluss-pk-hashcode-demo/tree/main/evidence).
## Root Cause
The sibling types in the same file, `Schema.hashCode()` and `Column.hashCode()`, hash only their value fields with no identity term; `PrimaryKey` is the only one that folds in `super.hashCode()`, which points to a slip rather than a design choice. (`constraintName` is intentionally excluded from `equals()`: it is a derived, non-persisted name that is regenerated on deserialize by `SchemaJsonSerde`. So the correct alignment is to hash the same field `equals()` compares.)
### Solution
Compute `hashCode()` over the same field `equals()` compares:
```java
public int hashCode() {
return Objects.hash(columnNames);
}
```
I have the one-line fix ready with a unit test (JSON round-trip) and an end-to-end ITCase (create table via the Admin API, read the schema back, use it as a hash key), both failing before the fix and passing after.
### Are you willing to submit a PR?
- [x] I'm willing to submit a PR!
Contributor guide
No contributing guide indexed for this repository
Research direction
Read fluss-common/src/main/java/org/apache/fluss/metadata/Schema.java, focusing on Schema.PrimaryKey.equals() and hashCode(), then run the JSON round-trip through Schema#toJsonBytes and Schema#fromJsonBytes or the linked SchemaHashCodeDemo.java reproduction. Done means equal primary keys and schemas have matching hash codes and a reloaded schema is found in a hash-based collection; the mentioned unit test and Admin API ITCase should pass.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java
- Domain
- databases
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 74/100