Skip to content

Fix all parquet_companion code-review findings (read/indexing path + merge + FFI) - #183

Open
schenksj wants to merge 6 commits into
mainfrom
fix/parquet-companion-indexing-review
Open

Fix all parquet_companion code-review findings (read/indexing path + merge + FFI)#183
schenksj wants to merge 6 commits into
mainfrom
fix/parquet-companion-indexing-review

Conversation

@schenksj

@schenksj schenksj commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

Resolves the code-review findings for the parquet_companion module across
two review documents (the read/indexing-path review and
PARQUET_COMPANION_INDEXING_REVIEW.md). All high, medium, low, efficiency,
and design findings are addressed
except a full de-triplication refactor
(D1), whose concrete drift bugs (M12, M14) are fixed directly. Every change is
covered by the 982 passing native lib unit tests plus new regression tests.

Two rounds of Sonnet code review were run against the changes; both rounds'
findings were addressed (notably: the F8 reserved-name check was narrowed to
avoid false-positives, and the M4 fast-field fix was reworked to keep the
index and read sides consistent — see "Review corrections" below).


High-severity (8/8)

  • F1 merged manifest row order now mirrors quickwit's combine_index_meta
    ([n-1, 0, …, n-2]), not input order.
  • F2 merge deletion guard reads the nested deletes.num_deleted_docs.
  • F3 manifest-combine failures fail the merge instead of silently producing
    a manifest-less split.
  • F4 hash_parquet_path uses stable xxh64, not DefaultHasher.
  • F5 indexing writer uses NoMergePolicy + wait_merging_threads() and
    rejects multi-segment output when fast fields are parquet-served.
  • F6 FFI add_arrow_batch validates column names/types, not just count.
  • F7 streaming producer panics surface as a channel error, not a clean EOF.
  • F8 name-mapping collisions and reserved __pq_* names return a clean
    error instead of panicking in SchemaBuilder.
  • M7 backstop: combined manifest row count must equal merged doc count.

Medium / low / efficiency / design

  • Config & schema — M1 one config-key convention (parquet→tantivy resolved
    once); M2 reject string-mode overrides on non-string columns; M3 Dictionary
    columns derived/decoded by value type; M4 IP/FixedSizeBinary/Decimal256 served
    correctly from parquet (new transcode arms) instead of empty/hard-error;
    M5/E7/L13 name mapping walks top-level fields, one-pass field-id index,
    validates explicit keys.
  • Merge & manifest — M8 storage_config compat check; M9 validate() covers
    first-file offset, file contiguity, segment coverage; L7 page-location length
    check; L11/D3 missing meta.json and hash-field mismatch are hard errors.
  • Statistics — M10 nan_count (surfaced through ColumnStatistics); L5
    reject stats on skipped/unknown fields; L6 unbounded-ceiling handling; L1
    UInt64 saturation; E4 fewer allocations.
  • Value conversion — M12 FixedSizeList/complex types serialize as JSON;
    M13 row-count mismatch hard-errors; M14 checked date/timestamp math; L9 u32
    TANT-offset overflow guard; L10 documented lossy Decimal128→f64; E3 share the
    manifest via Arc across batch fan-out.
  • FFI ingestion — M16 atomic-per-batch add; M17 finish_all_splits attempts
    all partitions + the JNI caller guards the registry teardown; M18 normalize
    timestamps before accumulation; M19 Date32 ISO stats; L14/L15 dead branch /
    stale comment; L16 bounded partition writers; E6 partition-key routing cache.
  • Indexing — E1 project only indexed columns; E8 parse JSON once; E2
    per-batch column resolution + concurrent hash touch-up; L2 char-safe error
    previews; L3 path-boundary check; L4 all-column offset-index requirement;
    L8 deterministic zero-row-file resolution.
  • Validation / misc — M6 fix cache poisoning on transient errors; M15
    range/date_range/extended_stats/percentile_ranks trigger transcode; L17 dead
    ES-style range removed; E5 no-match strip returns input unchanged; D6
    ES-shape range removed.

Review corrections (from the Sonnet passes)

  • F8 reserved-name validation narrowed to the two always-added __pq_* fields
    (companion _phash_*/*__uuids collisions are handled precisely at their
    creation sites), so legitimately-named columns aren't rejected.
  • M4 reworked: the first attempt made types native on the index side only,
    diverging from the read side. Instead the read side (record_arrow_value
    IpAddr arm, extract_bytes FixedSizeBinary, str-direct Decimal256) was
    fixed to match the unchanged field_source, and the Decimal256 transcode
    string form was aligned exactly with the index/retrieval formatting.

🤖 Generated with Claude Code

Addresses 15 findings from a code review of the parquet_companion
indexing path (indexing, transcode, schema derivation, arrow_ffi_import,
merge, hash rewriting/touchup, streaming FFI).

Correctness:
- hash_parquet_path: use stable xxh64 instead of std DefaultHasher, whose
  algorithm is unspecified across toolchains and would silently break
  doc->parquet resolution for existing splits.
- terms-agg include/exclude on hash-redirected fields: enlarge the size /
  segment_size sent to tantivy and truncate the post-filtered result back
  to the requested size, so rare included terms are not lost from an
  unfiltered top-N.
- transcode: error on a projected column missing from a parquet file
  instead of silently producing a malformed columnar (also fixes the
  empty-projection-reads-all-columns path).
- columns_to_transcode: skip non-"raw" tokenized text fields, which a
  parquet raw-string fast field cannot reproduce.
- doc/row pairing: RowSelection now selects each physical row once
  (duplicate-safe) and the TANT batch pairing maps decoded rows to all
  requesting indices, erroring on a true count mismatch instead of
  padding empty documents.
- arrow FFI import: split config is a single source of truth on the
  context (auto-rolled, rolled, and final splits now share one identity);
  add_arrow_batch validates column names/types, not just count.
- timestamp/Date32/Date64 conversions use checked_mul at all sites.
- Decimal256: error on f64 parse failure instead of silently yielding 0.0.
- merge: reject duplicate parquet relative paths across manifests.
- streaming FFI: build the fallible schema export before writing the array
  into consumer memory, avoiding a stranded array with a live release
  callback on schema-conversion failure.

Efficiency:
- hash touchup: run the per-hash parquet reads concurrently instead of
  serially.
- indexing hot loop: resolve column->tantivy-field mapping once per batch
  instead of per row.

Quality:
- remove dead convert_complex_to_json (duplicated convert_arrow_to_owned_value
  with divergent, incorrect coverage).

Adds regression tests for the terms-agg size-enlargement behavior. All
parquet_companion, arrow_ffi, aggregation, merge, and hash rewriter unit
tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Jul 5, 2026

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

@schenksj schenksj changed the title Fix parquet_companion indexing-side review findings Fix parquet_companion indexing-side review findings (fable) Jul 5, 2026
@schenksj schenksj changed the title Fix parquet_companion indexing-side review findings (fable) Fix parquet_companion read-side review findings (fable) Jul 5, 2026
Addresses the high-severity findings not covered by the prior review-fix
commit: the merge-integrity cluster (F1/F2/F3/M7), the writer merge policy
(F5), the streaming producer panic path (F7), and name-mapping collisions
(F8).

Merge integrity (merge.rs, merge_impl.rs):
- F2: the deletion guard read `num_deleted_docs` at the top level, but
  tantivy nests it under `deletes: Option<DeleteMeta>`, so the guard never
  fired. Read `seg.deletes.num_deleted_docs`; fix the test fixture to use
  tantivy's real shape.
- F1: combined-manifest row offsets concatenated in input order, but
  quickwit's combine_index_meta puts the LAST split's docs first
  ([n-1, 0, 1, …, n-2]). Concatenate manifests in that order so positional
  consumers (transcode, legacy retrieval, hash touch-up) map doc_ids to the
  right parquet rows. Update the two order-sensitive tests.
- M7: assert the combined row count equals the actual merged doc count as a
  backstop (combine_parquet_manifests now takes expected_merged_docs).
- F3: stop swallowing combine errors — a source split with a manifest that
  fails to combine now fails the merge instead of silently producing a split
  with no manifest (and thus no retrievable documents).

Indexing writer (indexing.rs):
- F5: set NoMergePolicy and call wait_merging_threads() so background merges
  can't reorder docs (breaking transcode's doc_id == parquet_row assumption)
  or GC segment files mid-bundle. Reject multi-segment output when fast
  fields are parquet-served, rather than transcode wrong values.

Streaming FFI (streaming_ffi.rs):
- F7: wrap the producer future in catch_unwind so a producer panic becomes an
  Err on the channel instead of a clean EOF that silently truncates results.

Schema derivation (schema_derivation.rs):
- F8: validate resolved tantivy field names before building the schema —
  reject duplicate mapped/identity names and columns colliding with reserved
  companion names (__pq_*, _phash_*, *__uuids) with a clean error instead of
  a SchemaBuilder panic across JNI. Adds regression tests.

All parquet_companion (339), schema_derivation (45), merge (11), and
arrow/streaming FFI (61) unit tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@claude

claude Bot commented Jul 5, 2026

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

Code review flagged that rejecting any column named `_phash_*` or `*__uuids`
is too aggressive: those companion fields are added only conditionally
(fingerprint on Hybrid Str fields; uuid tokenizer modes) and each already
handles a real collision precisely at its creation site — indexing.rs
disables the fingerprint for a shadowed field, and add_field_for_arrow_type
bails cleanly when a companion name collides. A legitimately-named column
like `session__uuids` or `_phash_temp` was being wrongly rejected.

Only `__pq_file_hash` / `__pq_row_in_file` are added unconditionally with no
guard, so restrict the reserved-name check to those two exact names. Update
the tests to assert `_phash_*`/`*__uuids` columns are now allowed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@claude

claude Bot commented Jul 5, 2026

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

@schenksj schenksj changed the title Fix parquet_companion read-side review findings (fable) Fix parquet_companion review findings (indexing/read path + high-severity merge/indexing) Jul 5, 2026
schenksj and others added 3 commits July 5, 2026 09:27
Addresses the remaining PARQUET_COMPANION_INDEXING_REVIEW.md findings beyond
the high-severity set (which two prior commits covered): the mediums, lows,
efficiency, and design observations across the indexing/merge/FFI paths.

Config & schema derivation:
- M1: field-scoped config (tokenizer_overrides, ip_address_fields, json_fields)
  is now normalized once from parquet-column-name keys to tantivy-field-name
  keys via the name mapping, so derivation and the indexing loop share one
  convention (skip_fields stays parquet-keyed as it applies pre-mapping).
- M2: reject compact string-indexing overrides (exact_only, text_uuid_*) on
  non-string columns instead of registering U64/ExactOnly against a numeric
  field and corrupting hash-rewrite reads.
- M3: Dictionary-encoded (categorical) columns are derived by and decoded to
  their value type at index and retrieval time rather than dropped as Unknown.
- M4: types the transcode path can't reconstruct from parquet (IP, Bytes,
  FixedSizeBinary, Decimal256) now always get native fast data in every mode,
  and the Bytes arm consults should_add_fast — fixing silently-empty IP
  aggregations (ParquetOnly) and hard errors (Decimal256/FixedSizeBinary,
  Hybrid). FixedSizeList added to the JSON type map.
- M5/E7/L13: name mapping walks top-level fields (Iceberg field-ids on group
  nodes now resolve), builds a one-pass field_id→column index, and validates
  that explicit mapping keys name real columns (typos no longer silent).

Merge & manifest:
- M8: reject merges whose source manifests have incompatible storage_config
  (only split[0]'s is retained).
- M9: manifest validate() now checks first-file row_offset==0, true file
  contiguity, and contiguous non-overlapping segment ranges covering total_rows.
- L11/D3: missing meta.json during merge is a hard error; string_hash_fields
  mismatch is a bail!, not a release-mode-noop debug_assert.
- L7: unpack_page_locations errors on a non-multiple-of-record length.

Statistics:
- M10: track nan_count separately (all-NaN vs empty), surfaced through
  ColumnStatistics; L5: reject stats requests for skipped/unknown fields;
  L6: no-ceiling truncation reports an unbounded max instead of a floor;
  E4: fewer allocations in observe_string; L1: UInt64 min/max saturates
  instead of wrapping negative.

Value conversion & retrieval:
- M12: FixedSizeList and other complex/unknown types serialize as JSON instead
  of an empty string; L9: guard u32 TANT offset overflow past 4 GiB;
  E3: share the manifest via Arc across batch fan-out instead of deep-cloning
  it per file.

FFI ingestion:
- M16: add_arrow_batch is atomic per batch (validate all rows before committing
  any); M17: finish_all_splits attempts all partitions and the JNI caller no
  longer tears down the registry entry before try_unwrap succeeds; M18:
  timestamps are normalized before accumulation; M19: Date32 stats use the ISO
  Date branch; L14/L15: dead branch removed / stale ownership comment fixed;
  L16: bounded partition-writer count; E6: cache the last partition-key routing.

Indexing efficiency & robustness:
- E1: project only indexed columns during parquet reads (skip wasted I/O on
  wide tables); E8: parse JSON once; L2: char-safe error previews; L3:
  path-boundary check in compute_relative_path; L4: require offset index on all
  columns; docid_mapping L8: deterministic zero-row-file resolution.

Validation module:
- M6: fix cache poisoning (transient storage errors no longer cached as
  absent); document the module is currently unwired. field_extraction M15:
  range/date_range/extended_stats/percentile_ranks trigger transcode; L17:
  dead ES-style range extraction removed. string_indexing E5: no-match strip
  returns input unchanged.

L10: documented the intentional lossy Decimal128→f64 shared design point.

All 982 native lib tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sonnet review of 79fd598 found three issues; all addressed.

Critical — M4 was half-done: the index-side should_add_fast change made
IP/Bytes/FixedSizeBinary/Decimal256 native, but the read/merge side
(field_source, merge_columnar_bytes, extract_string/extract_bytes,
record_arrow_value) still treated them as parquet-served, so the native data
was written then discarded and the target cases (IP-empty in ParquetOnly,
Decimal256/FixedSizeBinary hard-error in Hybrid) were not actually fixed.
Reverted the should_add_fast type additions (index/read now consistent again)
and fixed the failure modes on the read side instead:
- record_arrow_value gained an IpAddr arm (parse string → record_ip_addr) so
  IP columns transcode from parquet in ParquetOnly.
- extract_bytes handles FixedSizeBinary; the str-direct transcode path handles
  Decimal256 via value_as_string — removing the Hybrid-mode hard error.
- Kept the Bytes arm consulting should_add_fast (correct: native in Disabled,
  parquet-served otherwise, consistent with field_source).

Medium — E1 projection was dead code: build_column_mapping intentionally maps
every arrow column, so the projection set always equaled the full column count
and never applied. Build the projected indices from columns that are actually
indexed (not in skip_fields AND present in the derived tantivy schema), so wide
tables with skipped/unsupported columns are truly projected.

Low — M17 ref-count guard comment overstated the guarantee: added a second
strong_count check immediately before release_arc and reworded the comment to
acknowledge the (now negligible) residual race honestly.

All 982 native lib tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Re-review found the Decimal256 str-direct transcode arm used arrow's exact
value_as_string() (e.g. "123.4500000000"), while native indexing
(indexing.rs) and doc retrieval (doc_retrieval.rs) both format via the lossy
f64 form (e.g. "123.45"): scale 0 → raw i256 string, else parsed/10^scale.
The transcoded fast-field term would therefore never match the indexed term
or the retrieved value for the same row — a silent inconsistency that
replaced the prior hard error. Replicate the exact index-side formatting so
all three paths agree.

All 982 native lib tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@claude

claude Bot commented Jul 5, 2026

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

@schenksj schenksj changed the title Fix parquet_companion review findings (indexing/read path + high-severity merge/indexing) Fix all parquet_companion code-review findings (read/indexing path + merge + FFI) Jul 5, 2026
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