apache / apache/fluss

[common] Schema.PrimaryKey violates the equals/hashCode contract

Open
#3,628 1 comment 0 reactions 0 assignees View on GitHub
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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.