Skip to content

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

Description

@NestDream

Search before asking

  • I searched in the 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():

// 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

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

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<Schema> 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, with captured before/after logs under 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:

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?

  • I'm willing to submit a PR!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Fields

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions