Skip to content

feat(f32): Parquet and Arrow interoperability - #46

Merged
soldforaloss merged 8 commits into
mainfrom
feat/f32-parquet
Aug 4, 2026
Merged

feat(f32): Parquet and Arrow interoperability#46
soldforaloss merged 8 commits into
mainfrom
feat/f32-parquet

Conversation

@soldforaloss

Copy link
Copy Markdown
Owner

Summary

Adds first-class Apache Parquet and Apache Arrow interoperability — open and export typed columnar datasets while preserving types and nulls, with bounded memory on multi-gigabyte files.

Open (palette → "Open Parquet/Arrow…", drag-and-drop, or "Open file…" on .parquet / .arrow / .feather / .ipc):

  • An inspect dialog first: container format, row + row-group/batch counts, compression codec, the schema mapped to the F31 logical types (nested fields indented, timezones shown), and an editable-memory estimate — before anything loads.
  • Choose read-only (an indexed columnar backing with windowed reads over row groups / record batches and a bounded decoded-block cache, so the grid, filters, and export stay within bounded memory) or convert to editable behind an explicit memory check.
  • Type fidelity: signed/unsigned 64-bit integers (u64::MAX round-trips losslessly), exact decimal precision+scale, floats, booleans, dates, timestamps with time-zone metadata, and UTF-8 all survive; a NULL stays distinct from an empty string end to end (editable opens preserve the distinction via collision-free per-column null tokens).
  • Nested types: structs flatten to stable path-based column names; each list/map/struct field takes an explicit per-field policy (keep as JSON, explode into rows on an editable open, or drop).
  • Row-group statistics pruning: equality/range filters on numeric/date columns of indexed parquet documents skip whole row groups using their stats, with results identical to a full scan.

Export (palette → "Export as Parquet/Arrow…"):

  • Any scope (all rows, the filtered view, selected rows / columns / range) → Parquet (uncompressed, Snappy, or Zstd, with a configurable row-group size), an Arrow IPC file (Feather v2), or an Arrow IPC stream — a cancellable job through the atomic-save pipeline.
  • Typed export maps each column's declared logical type to the matching arrow type (Int64/UInt64, Decimal128 with unified precision/scale, Date32, µs/ns timestamps carrying the schema's time zone); null tokens and columnar NULLs export as real nulls distinct from empty strings.
  • Cells that cannot be represented under the declared types are written as NULL and counted into a per-column warning report; columns without a schema export as text verbatim.
  • A columnar document opens unsaved, so a later Save can never overwrite the binary source with CSV.

New backend modules parquet_arrow.rs (+3282) and columnar_export.rs (+1442); frontend inspect + export dialogs, typed invoke wrappers, palette entries, and store wiring.

Tests

  • Rust cargo test --lib: 728 passed, 0 failed (up from 726 on the base — the rebase pulled in two new highlight.rs unit tests from the parent's f42 Codex-fix). Includes 16 parquet_arrow unit tests (type-matrix extremes round-trip, arrow file/stream parity, row-group-stat pruning vs full scan, struct flatten, list/map policies, timezone preservation, convert memory-check, cancellation cleanup, null-vs-empty) and 21 columnar_export unit tests (scoped exact rows/cols, u64::MAX → uint64, decimal scale unification, compression codecs, subsecond → ns, zoned timestamps, unrepresentable → NULL + warning count, stale-revision rejection, null-vs-empty round-trip).
  • Frontend vitest: 392 passed (42 files), including 19 in columnar.test.ts.
  • Gates green on the first compile against the chain: npm lint / format:check / typecheck / build; cargo fmt --check; clippy --all-targets --all-features -- -D warnings.

Acceptance mapping

Acceptance criterion Where Evidence (unit test)
Open Parquet / Arrow IPC file (Feather v2) / Arrow IPC stream parquet_arrow.rs, ParquetInspectDialog.tsx arrow_ipc_file_and_stream_read_identically
Inspect-before-open: format, counts, codec, schema→logical types, memory estimate ParquetInspectDialog.tsx, parquet_arrow.rs inspect_reports_shape_types_and_compression, convert_memory_check_uses_the_columnar_estimate
Read-only indexed with windowed row-group reads and bounded memory document.rs, parquet_arrow.rs columnar_documents_behave_like_indexed_read_only_documents
Convert-to-editable behind an explicit memory check parquet_arrow.rs convert_to_editable_assigns_collision_free_null_tokens
64-bit signed/unsigned (u64::MAX lossless), decimal, float, bool, date, tz timestamps, UTF-8 parquet_arrow.rs, columnar_export.rs type_matrix_round_trips_extremes, u64_max_selects_uint64_and_round_trips_losslessly, timestamp_time_zone_metadata_is_preserved, zoned_timestamp_preserves_zone_metadata_and_instants
NULL distinct from empty string, end to end parquet_arrow.rs, columnar_export.rs null_vs_empty_string_round_trip_distinctly, columnar_document_re_export_preserves_null_vs_empty
Struct flatten to path names; list/map/struct per-field policy (JSON / explode / drop) parquet_arrow.rs struct_flattening_is_deterministic_and_escapes_dots, list_and_map_policies_are_deterministic, explode_multiplies_rows_in_the_editable_open
Row-group stats pruning identical to a full scan parquet_arrow.rs, filter.rs row_group_stats_skip_groups_and_match_the_full_scan, row_group_size_applies_and_pruned_filtered_reads_match_full_scan
Export any scope → Parquet (codec + row-group size) / Arrow file / Arrow stream, cancellable, atomic columnar_export.rs, ColumnarExportDialog.tsx scoped_export_writes_exact_rows_and_columns_in_order, parquet_compression_codecs_apply, arrow_file_and_stream_round_trip_like_parquet, cancelled_export_removes_all_output
Typed export logical→arrow; unrepresentable → NULL + per-column warning; schema-less → text verbatim columnar_export.rs unparseable_cells_export_null_with_warning_count, beyond_i64_with_negatives_warns_and_nulls, no_schema_exports_all_utf8, typed_false_keeps_cell_text_verbatim
Deferred results revision-guarded; cancellation leaves no leftovers columnar_export.rs, parquet_arrow.rs stale_revision_is_rejected_before_writing, cancel_stops_open_inspect_and_convert_without_leftovers

STACKED PR

This PR is stacked on feat/f42-highlight (its base branch), the tip of the chain main ← f31 ← infra ← f33 ← f37 ← f38 ← f48 ← f40 ← f41 ← f42 ← … ← feat/f42-highlight.

Review only the delta versus feat/f42-highlight — the 5 commits d8ebd5b…c9f53f8 (parquet/arrow read side; export + commands; UI; gates + docs; and the integration marker). GitHub's "Files changed" against this base already scopes to that delta; the parent chain's commits are not part of this review.

Integration: rebased cleanly onto feat/f42-highlight (80dfd60) with no conflicts — the parent's new Codex-fix commit touched only highlight.rs / Grid.tsx / HighlightRulesDialog.tsx, disjoint from every file f32 changes. No source fixes were required, so chore(f32): integrate onto chain is an empty marker commit.

🤖 Generated with Claude Code

soldforaloss and others added 5 commits July 18, 2026 00:25
Read engine for Parquet / Arrow IPC file (Feather v2) / Arrow IPC stream:

- arrow 59 (ipc) + parquet 59 (arrow, snap, zstd) locked deps.
- parquet_arrow.rs: format sniffing; inspection (rows, F31-mapped columns,
  row-group/batch count, codecs, nested fields, editable-memory estimate);
  ColumnarHandle — an indexed read-only backing with windowed reads over
  row groups / record batches and a bounded LRU of decoded text blocks.
- Typed-value -> canonical text: i64/u64 as decimal strings (never through
  JS numbers), exact decimal rendering from mantissa+scale, shortest-float
  display, true/false, ISO dates, naive/zoned timestamps (zone metadata on
  ColumnSchema.timeZone; zoned values render as the UTC instant), binary as
  hex. Columnar NULL is None in the tabular Option plane, Some("") stays an
  empty string; the grid text plane renders NULL as an empty cell.
- Nested: structs always flatten to escaped path-based names; lists/maps
  take an explicit policy (preserveJson / reject / explode-on-editable-open,
  single list column). Editable materialisation preserves null-vs-empty via
  collision-free per-column null tokens recorded in the schema.
- Document integration: Backing::Columnar wired through visit_rows /
  visit_at so the grid, filters, sorts and export work unchanged (backing
  reports indexedReadOnly on the wire on purpose); DocumentSource reads the
  Option plane for columnar docs; convert-to-editable and reindex commands
  branch for columnar documents (own memory estimate, token-preserving
  conversion, reindex refused).
- Filter acceleration: equality + range conditions on numeric/date columns
  prune parquet row groups via their statistics (conservative: only when
  typed bounds prove no match under an unchanged schema); matching_rows
  visits only surviving ranges and returns results identical to a full
  scan (tested).
- 16 unit tests: type matrix incl. i64::MIN/MAX, u64::MAX and decimal
  scale; null-vs-empty distinctness; tz metadata round-trip; flatten
  determinism + dot escaping; list/map policy matrix; explode semantics;
  null-token collision escalation; IPC file/stream equivalence; dictionary
  decoding; stats pruning correctness + graceful fallbacks; document
  integration; LRU budget; cancellation leaves no artifacts; inspection.

Export side, commands/UI and typed parquet/arrow writing land in the
follow-up F32 stages.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Write side of the Parquet & Arrow interop plus the F32 command surface.

- src-tauri/src/columnar_export.rs (new): scoped typed export engine.
  Any export scope streams to Parquet (uncompressed/Snappy/Zstd, optional
  row-group size bound) or Arrow IPC file/stream through the atomic-save
  pipeline as a cancellable job. Declared F31 logical types map to arrow
  types: Integer -> Int64, or UInt64 when a typing pre-pass finds values
  beyond i64::MAX and none negative (u64::MAX round-trips losslessly);
  Decimal -> Decimal128 with exactly unified precision/scale; Float ->
  Float64; Boolean; Date -> Date32; Datetime -> Timestamp us (ns when
  sub-microsecond digits exist) carrying ColumnSchema.time_zone as the
  arrow timezone; Text/Uuid/Json -> Utf8 verbatim. Rows read through the
  Option plane so columnar NULL stays distinct from the empty string;
  schema null tokens and empty typed cells export as null without warning;
  unparseable/unrepresentable cells export as null with per-column counted
  warnings on a job-keyed report.
- commands.rs: columnar_inspect, columnar_open_indexed (job-registered
  read-only document), columnar_open_editable (memory check + force,
  explode-capable, opens unsaved with no path so Save cannot clobber the
  binary source), columnar_export (revision-guarded, fail-fast scope
  validation), get_columnar_export_report. Converting an open columnar
  document to editable now also detaches path/fingerprint and marks it
  derived-unsaved for the same reason.
- parquet_arrow.rs: ColumnarFormat is now Deserialize so export options
  share the inspection wire names.
- 21 new tests: null-vs-empty distinctness both directions, i64/u64
  extremes, warning policy, decimal scale unification and exact columnar
  round trip, zoned timestamp metadata + instants, ns unit selection,
  untyped/typed-false verbatim text, scope exactness, empty scope, all
  three containers, compression codecs, row-group bound + stats-pruned
  filtered reads equal to full scan, cancel cleanup, stale revision.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Frontend for the F32 Parquet/Arrow interop backend (parts 1-2): inspect +
open dialog, scoped columnar export dialog, open-pipeline wiring, and the
TS/store surface.

- types.ts: mirror the Rust DTOs (ColumnarFormat, ComplexPolicy,
  ColumnarOpenOptions, InspectedColumn, ColumnarInspection,
  ColumnarCompression, ColumnarExportOptions, ColumnWarning,
  ColumnarExportReport).
- tauri.ts: columnarInspect / columnarOpenIndexed / columnarOpenEditable /
  columnarExport / getColumnarExportReport wrappers.
- lib/columnar.ts (+ test): pure logic — format/compression labels,
  suggested export names, complex-field policy state, and the open-mode
  plan (explode forces editable, one field max). 19 vitest cases.
- store: columnarOpen inspect-flow slice and columnarExportResult slice with
  actions; openPath routes .parquet/.arrow/.feather/.ipc/.arrows through the
  inspect dialog; columnar opens hand off to the shared "openIndexed" job so
  the existing completion path adds the tab. Progress reports rows (new
  IndexingState.unit) so the open-mode modal labels them correctly.
- ParquetInspectDialog: format/counts/codec/memory summary, F31-mapped
  schema table with nested indentation and timezones, per-field policy
  pickers, indexed vs convert-editable choice behind a memory warning.
- ColumnarExportDialog: format + Parquet compression/row-group-size + typed
  toggle + scope, with a done phase surfacing per-column invalid-cell counts.
- commandDefs: "Open Parquet/Arrow…" and "Export as Parquet/Arrow…".
- CHANGELOG + README entries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Consolidate the three staged F32 CHANGELOG entries (read engine, typed
export & commands, UI) into a single user-facing "Parquet & Arrow
interoperability" entry under [Unreleased]. README bullets already fit
their Features groups. All gates green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rebase feat/f32-parquet onto feat/f42-highlight (80dfd60). The new f42
commit (codex review — cross-column projection, stale overlays, clear-all
draft) touched only src-tauri/src/highlight.rs, src/components/Grid.tsx,
and src/components/HighlightRulesDialog.tsx — disjoint from every file f32
changes, so the replay was conflict-free. Full gates green on the first
compile against the chain (npm lint/format/typecheck/test 392, build;
cargo fmt/clippy; cargo test --lib 728, up from 726 as the rebase pulled in
the two new highlight.rs unit tests) — no source fixes required.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c9f53f82bd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src-tauri/src/commands.rs
if !force {
let probe = path.clone();
let inspection = tauri::async_runtime::spawn_blocking(move || {
crate::parquet_arrow::inspect(Path::new(&probe), None)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recompute the editable guard for explode policies

When the user selects explode, open_editable_rows can materialize one output row per list element, but this guard reuses inspect with the default preserve-as-JSON projection, so needsDecision is based only on the original row count and data bytes. A small file with a very large list column can therefore skip the confirmation and allocate far more rows than estimated; the guard should account for the selected options or force confirmation when any explode policy is present.

Useful? React with 👍 / 👎.

Comment thread src/store/useStore.ts
Comment on lines +2773 to +2775
if (isColumnarPath(path)) {
await get().openColumnarInspect(path);
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid routing project restores into the inspect dialog

Project open applies each saved source by calling openPath, but this new route only opens the interactive Parquet/Arrow inspect dialog and returns without creating a tab. As a result, a project saved with a read-only columnar source restores with that tab missing until the user manually confirms the dialog, while the project baseline is still updated; project restore needs a non-interactive columnar open path or should bypass this dialog.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in e70bfab.

applyProjectPlan restored every source through openPath, which routes any Parquet/Arrow path into the interactive inspect dialog (openColumnarInspect) and returns without creating a tab — so a restored columnar source was left missing until the user manually confirmed the dialog, while the project baseline had already advanced.

Fix: project restore now routes columnar entries through a new non-interactive openColumnarRestore store action that reopens the source directly as an indexed read-only document with default policies (no dialog), awaiting the open job so the tab exists before the restore's tab-order / annotation / view-reapply steps run. Indexed read-only is the memory-safe, prompt-free reproduction of a columnar source: it never hits the F10 memory decision, and per the spec invariant an indexed document stays read-only unless explicitly converted.

The routing decision is extracted as a pure restoreOpenRoute() helper in src/lib/project.ts, covered by unit tests in project.test.ts (columnar extensions → columnarIndexed; csv/json/jsonl/tsv/zip/gz → standard).

Note: the project schema records no columnar open mode/policy (only delimiter/encoding/hasHeaderRow), so a faithful editable-vs-indexed round-trip would be a separate schema change; indexed read-only is the correct non-interactive default for restore. Gates green (npm lint/format/typecheck/build + 394 vitest; cargo fmt/clippy + 728 lib tests).

…ely on project open

A project referencing a columnar (Parquet/Arrow) source restored it through
openPath, which routes columnar files into the interactive inspect dialog and
returns without creating a tab. The source was left missing until the user
manually confirmed the dialog, even though the project baseline had already
advanced — project restores must be non-interactive (F37).

applyProjectPlan now routes columnar entries through a new openColumnarRestore
action that reopens the source directly as an indexed read-only document with
default policies (no dialog), awaiting the open job so the tab exists before the
restore's tab-order / annotation / view-reapply steps run. Indexed read-only is
the memory-safe, prompt-free reproduction of a columnar source and never hits
the F10 memory decision (indexed documents stay read-only unless converted). The
routing decision is extracted as a pure restoreOpenRoute() helper with unit
tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@soldforaloss
soldforaloss changed the base branch from feat/f42-highlight to main August 4, 2026 19:40
@soldforaloss
soldforaloss merged commit 1a24b7f into main Aug 4, 2026
2 checks passed
@soldforaloss
soldforaloss deleted the feat/f32-parquet branch August 4, 2026 20:01
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