Skip to content

Fix key hashing, collection equality, and compaction from a stale handle - #25

Merged
codeboost merged 6 commits into
masterfrom
dates-and-keyshashes-fixes
Sep 15, 2026
Merged

codeboost merged 6 commits into
masterfrom
dates-and-keyshashes-fixes

Conversation

@codeboost

@codeboost codeboost commented Sep 11, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes four correctness bugs found in the 2026-09-10 database audit. All were reproduced through the public API before being fixed test-first. The database is still in development, so no migration is provided for the previous key encoding; instead, files written with the old encoding are now rejected on open.

  • Date keys lost millisecond precision. java.util.Date keys were hashed from their display string, which drops milliseconds and depends on the default timezone. They now hash from epoch milliseconds. Date subclasses such as java.sql.Timestamp are rejected as keys rather than silently truncated.
  • Collection keys used printer output as identity. Keys like [1 2] were hashed from pr output, so *print-length*, map iteration order, and wrapper print methods all changed the stored identity. New xitdb.util.key-hash namespace hashes collections canonically: type tags, fixed-width child digests, and sorted digests for maps and sets. Native and database-backed collections share the encoding.
  • Wrappers broke Clojure's equality and hashing contracts. Read and write views now implement IHashEq, hashCode, equals, MapEquivalence, and the java.util.Map/Set interfaces, so distinct, set, group-by, and native hash-map lookups work on stored collections, and = holds in both operand orders. Keys the database cannot encode (symbols, ratios, arbitrary objects) read as absent from containsKey and get, so comparing a native map that holds such a key against a stored view returns false instead of throwing.
  • compact could publish an empty copy of a populated file. A handle opened before another handle initialized the file still carried an uninitialized writer header. compact now refreshes the source writer's header before copying.

Key-hash format is now versioned. The database header records the Clojure key encoding as clj1. Opening an older, unversioned file throws with instructions to export using the version that created it and import into a new database. The check does not modify the file, and compaction preserves the header ID. The README documents the upgrade path. A fixture written by master with xitdb 0.38.0 is checked in under test-resources to pin the rejection.

Also: top-level Double keys are now accepted and hash the same way as nested doubles, dead branches in the scalar key-hash path are removed, and the xitdb Java dependency is bumped to 0.39.0.

Test plan

  • clojure -M:test: 235 tests, 2,989 assertions, 0 failures on xitdb 0.39.0
  • New tests cover millisecond dates across reopen, timezone independence, printer-setting independence, equal keys in different iteration order, reusing stored keys, Date subclass rejection, double keys across reopen, wrapper equality/hash in both operand orders and after in-transaction mutation, compaction from a handle opened before initialization, unsupported keys comparing unequal in both directions for hash and sorted maps, rejection of a legacy file without modifying it, and the current format surviving reopen and compaction

🤖 Generated with Claude Code

codeboost and others added 5 commits September 11, 2026 13:41
Hash Date keys by epoch milliseconds to prevent collisions and timezone-dependent lookups. Reject Date subclasses that could lose precision.

Replace printed collection-key hashes with canonical structural encoding, independent of printer settings, iteration order, and wrapper types.

Implement value equality and hashing across read and transaction views so distinct, group-by, sets, and map lookups produce correct results.

Add regression tests, document hashing helpers, and name encoding tags.
keySet, values, and entrySet on the map wrappers copied the whole map
into a native Clojure map on every call. That lost sort order on sorted
maps past eight entries, returned snapshots instead of views, and threw
AbstractMethodError on the write wrappers.

The throw is a pre-existing hole: IPersistentMap extends Iterable, but
XITDBWriteHashMap, XITDBWriteSortedMap, and XITDBWriteLinkedArrayList
never implemented iterator, so reduce and into failed on any of them
inside a transaction. They now iterate their own seq.

The views are now small AbstractSet/AbstractCollection proxies backed by
the wrapper's seq, count, containsKey, and entryAt, modeled on
clojure.lang.APersistentMap, shared from xitdb.util.collection.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Stamp "clj1" into the database header so files written with the old
printer-dependent key hashes are rejected on open instead of silently
missing every collection and Date key. The check does not modify the
file, and compaction preserves the header ID. README documents how to
export and re-import old files.

Native and Java map equality call containsKey on the stored view with
keys the database cannot encode (symbols, ratios, arbitrary objects).
Those now read as absent instead of throwing, so = and equals return
false in both operand orders. Map.get goes through entryAt for the
same reason. Storage failures still propagate.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@codeboost
codeboost added this pull request to stack #27 September 15, 2026 08:40
@codeboost
codeboost merged commit 81057da into master Sep 15, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant