# Known limitations and disabled tests This document inventories every `@Disabled` / `@Ignore` test in `sirix-core`, explains *why* it's off, and points at the tracking artifact (issue, documented gap, or follow-up plan). The point: a future reader should never have to guess whether a disabled test is hiding a real bug. Categories used: - **bench/manual** — long-running performance benchmark. Disabled in CI; expected to be removed locally and run on-demand. **Not** a correctness gap. - **manual-utility** — helper tooling (e.g., dumps a canonical encoded frame for an external bench). Not a regression risk. - **known-gap** — real correctness or capability gap, intentionally disabled until the underlying feature lands. Each entry below names the tracking doc. - **legacy** — refers to a code path that has been superseded; the test documents behavior of removed/replaced code and is kept only for archeology. Should be deleted on next cleanup pass. --- ## sirix-core | Test | Category | Notes | |---|---|---| | `service/xml/xpath/FunctionsTest.java:217-450` (19× `@Ignore`) | legacy | Tests for *not-yet-implemented* XPath 1.0 functions in the sirix-native XPath axis. XQuery is now dispatched through Brackit; this legacy axis is no longer the canonical query path. Safe to delete on a cleanup pass — they document feature gaps of a deprecated component, not active bugs. | | `service/json/shredder/JsonShredderTest.java:74,154,209,272` | bench/manual | Four Chicago-dataset shredder benchmarks (3.6 GB JSON corpus, not in CI). Use `removeAnnotation` locally + ≥ 16 GB RAM. | | `service/json/shredder/JsonShredderTest.java:573` | known-gap | "Duplicate keys" — JSON spec allows duplicate object keys; Sirix's shredder de-duplicates. Test documents the behavior gap; no current customer ask. | | `service/json/shredder/JacksonJsonShredderTest.java:417` | known-gap | Same "Duplicate keys" gap as above (Jackson variant). | | `service/json/shredder/JacksonJsonShredderTest.java:562,597,608` | bench/manual | Manual large-file shredding benchmarks. | | `access/node/json/JsonNodeTrxInsertTest.java:45,89` | bench/manual | 650 000-document insertion benchmarks with `println` instrumentation — pure scratch tests. Should be tagged `@Tag("benchmark")`. | | ~~`index/IndexIntegrationTest.java:1339`~~ | re-enabled | NAME-with-HOT variable-length key serialization issue — verified to pass against the current HOT strict-Binna conformance work (see `docs/HOT_PAPER_IMPOSSIBILITY.md`). Test re-enabled this session. | | ~~`index/hot/HOTFormalVerificationTest.java:3123`~~ | re-enabled | Was "SirixIOException: leaf page capacity exhaustion at high-dup counts". Root cause found while fixing the projection grow-rebuild bug: stale swizzled `PageReference` copies kept reading `HOTLeafPage` instances the TIL had closed after a CoW overwrite (use-after-close on recycled 64 KiB frames → garbage keys → runaway splits / spurious capacity exhaustion). `PageReference.getPage()` now treats a closed HOT leaf as a cache miss and callers re-resolve via `logKey`. Test re-enabled and green (≈72 s). | | ~~`index/hot/HOTFormalVerificationTest.java:3198`~~ | re-enabled | Was "FrameSlotAllocator size-class-4 exhaustion at 100K random" — same stale-swizzle use-after-close root cause as :3123 (the runaway splits exhausted the 64 KiB frame class). Re-enabled and green (≈3 s). | | `index/hot/HOTMicrobenchmark.java` (class-level) | bench/manual | HOT microbenchmark suite. Designed to pollute the regular suite — enable manually. | | `index/hot/HOTIndexIntegrationTest.java:685` | (not actually disabled) | The `@Disabled` here is in a comment, not on a test. False positive in `grep`. | | `page/SirixLZ77DumpEncodedTest.java:22` | manual-utility | Utility that dumps a canonical encoded frame for the standalone C bench. Not a test. | | ~~`io/ChecksumVerificationTest.java:295`~~ | re-enabled | 4 `SirixCorruptionException` constructor tests — re-enabled this session; all pass. | | ~~`access/AsyncAutoCommitTest.java:asyncAutoCommit_underDocumentedConstraints_works`~~ | re-enabled | Surfaced the `KeyedTrieWriter.prepareIndirectPage:176` ClassCastException under `KEEP_OPEN_ASYNC_FLUSH` — root cause was a cross-generation `logKey` collision in `TransactionIntentLog.put`. Fixed this session by adding an `activeTilGeneration == currentGeneration` guard before reusing `existingKey`. Test now passes. | ### Async pre-flush (`KEEP_OPEN_ASYNC_FLUSH`) caveats - **Rollback + preallocated commits (NOW ON BY DEFAULT):** an async-flushed transaction that ROLLS BACK leaves its already-appended (never-published) pages in the data file, and pages the transaction re-read from those offsets in the shared record/fragment caches. Under the legacy grow path (offset derivation from physical file size) the orphaned region is never reused and the stale cache entries are unreachable. With preallocated commits — the default since the durable-commit fast path was enabled by default — the next writer re-derives its append frontier from the last committed revision and can REUSE those offsets: a retried import then writes different pages at offsets the caches still associate with the aborted transaction's content. Until per-resource cache invalidation on rollback lands, run async-flush bulk imports that may be retried after failure with `-Dsirix.commit.preallocated=false`. - **Crash durability:** async-flush imports mint NO intermediate revisions — nothing of the import is durable until the single final commit publishes. The synchronous auto-commit mode (`-Dsirix.import.asyncFlush=false`) restores progress-checkpoint revisions where a crash loses only the tail. ## Platform - **Windows + MEMORY_MAPPED storage: interrupted-first-commit recovery cannot re-initialize in place.** Windows hard-locks memory-mapped files, so the recovery path that truncates and re-bootstraps a damaged bootstrap-only resource fails while any mapping may still be live. Use the default `FILE_CHANNEL` backend on Windows (recovery is fully covered there); the corresponding `InterruptedFirstCommitRecoveryTest` cases are skipped on Windows. ## Disabled-but-by-design A handful of test classes ship `@Disabled` at the class level intentionally — they're soak / stress harnesses gated behind a system property. These are **not** in the table above because they're CI-correct by design: - `stress/BitemporalSoakStressTest.java` — soak driver, gated on `-Dhot.soak.run=true`. Default path is `@EnabledIfSystemProperty`; class-level `@Disabled` is absent. - `index/hot/HOTSoakTest` family — similarly gated. ## CI-enforced The full sirix-core test suite (with the disabled tests excluded) runs on every PR. Disabled tests count toward "skipped" in the JUnit XML output but do not block CI. A future improvement: a CI step that asserts the count of `@Disabled` does not increase between commits without an accompanying entry here. ## Vectorized executor (analytical fast paths) The `SirixVectorizedExecutor` fast paths (group-by, filtered count, aggregates, count-distinct, multi-key group-by) only claim a pipeline when the brackit detection proves the query's shape matches what the executor emits — anything else falls back to the generic (always correct) pipeline. ### Fixed gaps (no longer limitations) - **Moved subtree kept its old pathNodeKey when the target path already existed (path summary).** `PathSummaryWriter#processFoundPathNode` (the merge branch of `adaptPathForChangedNode`, shared by XML renames and JSON subtree moves) bumped reference counts and adapted DESCENDANT NameNodes but never reset the moved/renamed node's own `pathNodeKey` — only the create-new-path branch did. First move to a fresh path: correct; second move to the now-existing path: the moved record still claimed the OLD record set's path, so path-scoped scans attributed it to the wrong root (reference counts were right, node attribution wasn't). The merge branch now resets the root's `pathNodeKey` like the create branch. Caught by the `ProjectionIndexStressTest` tombstone→rebuild cycles (cycle 1's fallback answered 54 instead of 52). - **Unscoped name sweep after re-open (generic aggregate path).** The path-scoped aggregate resolves its target path node by matching named ancestors, but read `PathNode#getName()` directly — which is only populated for path nodes created in the CURRENT process. On a deserialized summary every named ancestor looked anonymous, resolution failed, and the aggregate fell back to an UNSCOPED name sweep: `sum($doc.records[].age)` also counted `age` fields under OTHER roots (e.g. records moved out to an archive array). Names now resolve through the positioned `PathSummaryReader` (nameKey lookup), with the `__array__` sentinel treated as anonymous so fresh and re-opened summaries walk identically. Caught by the `ProjectionIndexStressTest` tombstone→rebuild cycle (post-tombstone fallback answered 55 instead of 54). - **Stale in-process registry serving after invalidation (projection paths).** The committed lookup fell back from the revision-scoped catalog to the in-memory `ProjectionIndexRegistry`, whose handles are gated only by `validFromRevision` — so after an invalidating commit (e.g. a subtree move out of the record set) a warm registry handle installed at create time kept serving the PRE-tombstone snapshot to every later revision in the same process. The catalog is now authoritative whenever the resource carries catalogued projection definitions at the executor's revision: a catalog miss there means "not usable here" and the registry may not answer (it remains only for bench/test wiring without catalogued definitions). Caught by the `ProjectionIndexStressTest` tombstone→rebuild cycle soak. - **Sparse fields (projection paths).** Projection leaves now carry per-column presence bitmaps + per-column "unrepresentable value" flags (leaf format v2, self-describing tail — see `ProjectionIndexLeafPage`). Predicates over a missing field are false (the stored default never matches), group-by routes missing keys to the missing bucket (`'m'` segment / null group key), aggregates skip missing rows. Columns that ever saw null/object/array/ kind-mismatched values are flagged and the projection paths decline them (typed kernels / generic pipeline answer correctly instead). - **Sparse group fields (scan path, single key).** The typed kernel synthesizes the missing-key group from `recordCount - visited` for top-level-array sources (this used to be a loud `QueryException`). - **OR/NOT over a missing anchor field.** Brackit's detection now refuses any predicate without a *sound anchor* — a referenced field whose absence provably falsifies the predicate (`PredicateNode.findSoundAnchorField`, with correct three-valued Not/De-Morgan handling). `a > 1 or b > 1` runs on the generic pipeline; same-field ORs still vectorize. The executor re-checks at compile and fails loudly if an unsound tree slips through (e.g. an old brackit). - **Double/decimal predicate literals.** `where $u.score > 9.99` was silently truncated to `> 9` (detection called `Number#longValue()` on xs:double and xs:decimal literals). Detection now emits exact leaves: `NumCmp` for long-representable literals, `FpCmp` for finite xs:double (double-space comparison — the interpreter's own promotion, including its precision loss for integers above 2^53), `DecCmp` for other xs:decimal literals carrying the exact `BigDecimal`. The scan path evaluates long/double/decimal document values per the interpreter's per-type dispatch; the projection path rewrites fractional thresholds over provably-integral columns into exact long-space predicates (`x > 9.99 ⟺ x >= 10`, verified by brute force against the promotion oracle in `FpCmpIntegralRewriteTest`). - **Double projection columns (since the segment-directory redesign).** `jn:create-projection-index` accepts `double`/`decimal` column types; cells store exact doubles in an order-preserving encoding (predicate literals transform at plan time, aggregates surface `xs:double`). The value-exactness gate is fail-closed: a column that ever absorbed a lossy `BigDecimal`→double conversion declines value-exact serving and falls back to the generic pipeline. **Value aggregates (sum/avg/min/max) are served only under the pure-double-source provenance bit** (`COLUMN_FLAG_PURE_DOUBLE_SOURCE`, flags bit2): every cell of the column must have shredded from a `Double` source (in practice, JSON exponent-form literals like `1.25E0` that round-trip through `Double.toString`), under which the interpreted fallback provably accumulates in double space and types the result `xs:double`. Served sums/avgs use a seed-first document-order fold (bit-identical to the interpreter's pairwise fold, including lone `-0.0` and ill-conditioned association-order cases) and served min/max use `Double.compare` total order (`-0.0 < 0.0`, like the interpreter's comparator). Plain JSON decimals (`1.25`) shred as `BigDecimal`, the fallback accumulates them decimal-exactly (`Dec`-typed), and those columns stay count-only — as do integer-fed double columns (exactness is not the bar; result TYPE parity is) and `Float`-fed ones (the fallback types those `xs:float`). Predicates (incl. promoted decimal literals) and counts are served regardless. Double BODY streams **ALP-compress** when profitable (width escape 65: decimal scale pair + FOR-packed digits + verbatim exceptions, every cell verified bit-exact at encode time); non-decimal data falls back to plain FOR over the transformed bits byte-identically to before. Width bytes 66–255 remain reserved escapes. - **Legacy (pre-descriptor and pre-segment-slot) projection stores.** The segment-directory layout replaced chunked storage, and the **segment ⇔ slot** layout (one HOT slot per segment) then replaced the descriptor layout — both without a metadata version bump (no deployed databases). A rebuild detects either structurally — slot-0 payload is not a blob marker (chunked), or a row group sits at a RAW slot id the segment-slot layout never writes (descriptor) — and swaps in a fresh sub-tree. That reset is mandatory rather than tidy: writing composite-keyed row groups into a raw-keyed sub-tree aliases raw slot 65536 onto composite key `(rowGroupId=1, slotKind=0)` at scale, which is unrepairable. Old pages remain on disk (append-only store) but are unreachable from new revisions; a resource copy/re-import sheds them. There is now exactly one layout and no code path that can produce another, so no future store can land in either legacy state. - **Mixed int/double columns under predicates.** Document doubles are no longer truncated to longs during predicate evaluation (the `rating` 3 vs 3.7 family), and the NumberRegion zone-map page prune now requires the tag to cover EVERY anchor slot before skipping a page (a long-only region says nothing about double-valued rows). - **Mixed-type numeric group keys merge like the interpreter.** The typed key encodings canonicalize: integral doubles below 2^53 and long-representable integral decimals encode in the long key space; decimals equal to their shortest double form encode in the double image space; remaining decimals are scale-canonicalized (`2.5` ≡ `2.50`). So `18`, `18.0` and `18.00` are ONE group, matching the generic pipeline's `hash(doubleValue)` + `atomicCmp` equality. Mixtures whose interpreter grouping is ORDER-DEPENDENT fail LOUDLY instead: long keys + integral doubles at/above 2^53 (one double is `atomicCmp`-equal to several distinct longs — probe-verified order-dependent merging), long keys + non-shortest-form decimals with small integral images, and double keys image-colliding with non-shortest-form decimals (the interpreter merges them but renders the FIRST tuple's lexical). A `-0.0` double group key also fails loudly (interpreter merges it with `0` but renders first-tuple lexical) — unreachable from JSON ingestion, which loses the zero sign at shred time. - **Negative-hash anchors now use the page-skip registry.** nameKeys are String hashes and may be negative (`active`, `amount`); the scheduler's publish/lookup guards exclude only the `-1` missing sentinel, so scans anchored on negative-hash fields populate and reuse the page-skip bitmap like everyone else (was: permanent full page sweeps). - **Multi-key group-by with a sparse anchor — re-anchor on a PROVABLY-DENSE group field (scan path).** An anchor-based scan never visits a record missing the anchor field, so a SPARSE anchor used to fail loudly even when another group field was present on every record. The unpredicated multi-key kernel now chooses the scan anchor among the group fields by PROVABLE density: a field is dense iff the path summary's OBJECT_KEY reference count for its (unambiguously resolved, query-scoped) path equals the top-level array's record total — `getReferences()` is incremented once per referencing node, and for a top-level array of objects a key occurs at most once per record, so equality proves presence on every record. The scan then anchors on the dense field, visits every record, and the remaining (possibly sparse) keys fall out as their values, including the `'m'` missing bucket the typed encoder already emits. Works for the dense field in ANY group-key position and across provenance (jn:store / shredder). Density is never guessed: if NO group field is provably dense (path summary off, nested source, ambiguous path, or every candidate genuinely sparse) the kernel keeps the loud bail / projection fallback. It also REFUSES to re-anchor when the FIRST group field is absent on EVERY record — brackit's interpreter collapses such a grouping to a single all-null tuple (an empty FIRST grouping key swallows the whole tuple), which a dense-anchored scan cannot reproduce, so that shape fails closed. - **Aggregate edge semantics.** `avg`/`min`/`max` over zero contributing rows return the empty sequence (was a fabricated 0); `count(... return $u.f)` counts non-empty derefs of ANY value type (was: numeric values only, and ignored `f`'s presence under predicates). - **Decimal-valued aggregate fields.** `jn:store`/`JsonShredder` keep plain fractional JSON literals as BigDecimal (only round-trippable exponent-form literals become compact doubles), and the typed redo path folded them via `Double.parseDouble` while the interpreter sums xs:decimal exactly and divides via `Dec#div`. The `MixedAgg` accumulator now folds decimal rows exactly (order-free — parallel folds cannot drift) and delegates the avg division to brackit for digit-for-digit parity. The companion brackit fix (branch `feat/fpcmp-predicates`) repairs `divideBigDecimal`'s scale rule, which HALF_EVEN-rounded terminating quotients away (`1.0 div 2.0` = 0, `10.2 div 4` = 2.6). ### Remaining limitations - **Multi-key group-by where NO group field is provably dense (scan path).** Re-anchoring (see fixed gaps) handles a sparse anchor whenever ANOTHER group field is provably dense. When density cannot be proven for ANY group field — every candidate genuinely sparse, the source is nested (record total not cheaply known), the path summary is off, or the path is ambiguous — the anchor walk still cannot reconstruct the secondary key values of unvisited records, so it fails LOUDLY (`QueryException` naming the anchor field). A covering projection index serves the same query correctly (presence bitmaps see every record). Likewise an entirely-absent FIRST group field fails closed: the interpreter collapses that grouping to one all-null tuple, which the scan cannot reproduce. - **Nested sources with sparse group fields (scan path).** The record total is only cheaply known for top-level array sources; for nested sources a sparse single group key still yields silently-partial groups (pre-existing, unchanged). Projection-backed queries are exact. - **Non-numeric aggregates.** `min`/`max`/`sum`/`avg` over fields holding only strings/booleans/nulls fail LOUDLY (the interpreter applies string/error semantics the numeric kernels cannot reproduce; historically this silently returned 0). Mixed numeric/non-numeric columns keep the legacy "skip non-numeric values" fold, which diverges from the interpreter's type error — unchanged. - **Double-bearing aggregate columns and parallel summation order.** Once a column contains xs:double rows the interpreter's running sum becomes double and the sequential fold order matters at the last ulp; the parallel accumulator re-associates additions, so adversarial double data can differ in the final ulp. Exact for binary-fraction values (the differential gate's data); pure long/decimal columns are immune (exact, order-free folds). - **`xs:float` document values in predicate fields** fail loudly: the interpreter compares xs:float operands in FLOAT space (`Float.compare`), which double-space evaluation cannot reproduce. JSON ingestion has not produced floats since the alpha13 narrowing removal. - **Legacy (pre-presence) projection leaves** carry no presence information; every projection fast path declines them (scan kernels answer instead). Rebuild persisted projections with `-Dsirix.projection.forceRebuild=true` to migrate to the v2 leaf format. The `TypedGroupByDifferentialTest` suite (89 cases) pins vectorized ≡ interpreted for the supported shapes, including typed (numeric/boolean/double) and multi-key group keys, the negative-hash nameKey regressions, adversarial sparse shapes (missing-on-30%, missing-on-all, present-but-null, mixed-kind columns, sparse group keys/aggregates/predicates, OR/NOT-over-sparse), the double/decimal predicate family across scan and projection paths, exact decimal/genuine-double aggregate parity (jn:store vs shredder provenance), and the multi-key dense-anchor selection (sparse anchor + dense second key in both orders, dense anchor as the 2nd/3rd key, numeric dense anchor, mixed provenance, plus the fail-closed cases: both-sparse-no-projection loud bail with the same query correct via a covering projection, and absent-first-key loud bail). ## Cleanup actions queued 1. Delete the 19 legacy XPath `@Ignore` tests (`FunctionsTest.java:217-450`) — they're 14+ years stale and refer to a non-canonical code path. 2. ~~Re-enable or remove `ChecksumVerificationTest.CorruptionExceptionTests`~~ **Done this session.** 3. Tag the bench/manual tests with `@Tag("benchmark")` so they can be excluded via JUnit Platform configuration rather than `@Disabled`.